<?php

namespace Velis\Bpm\Ticket;

use ArrayObject;
use Company\Company;
use Exception;
use Psr\SimpleCache\InvalidArgumentException;
use ReflectionException;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Email\Message;
use Velis\Bpm\Ticket\Post\File;
use Velis\Bpm\Workflow\Event;
use Velis\Bpm\Workflow\Subject;
use Velis\Db\AnyValue;
use Velis\Dictionary;
use Velis\Exception as VelisException;
use Velis\Exception\BusinessLogicException;
use Velis\Filter;
use Velis\Label;
use Velis\Lang;
use Velis\Model\DataObject;
use Velis\Model\RelationLoader\Exceptions\NotLoadedRelationException;
use Velis\Model\RelationLoader\Relations\HasManyThroughRelation;
use Velis\Model\RelationLoader\Relations\HasOneRelation;
use Velis\Model\Sanitizable;
use Velis\Output;
use Velis\ParameterBag;
use Velis\User;

/**
 * BPM ticket base model
 * @author Olek Procki <olo@velis.pl>
 *
 * @property string $ticket_status_id
 */
class Ticket extends DataObject implements Subject, Sanitizable
{
    use Mixin\AccessTrait;
    use Mixin\CompanyTrait;
    use Mixin\CompareTrait;
    use Mixin\HistoryTrait;
    use Mixin\LinkTrait;
    use Mixin\NotificationTrait;
    use Mixin\ObserverTrait;
    use Mixin\PersonTrait;
    use Mixin\StatusTrait;
    use Mixin\WorkflowTrait;
    use Mixin\VisitTrait;


    /** Common statuses */
    public const OPENED    = 'Opened';
    public const CONFIRMED = 'Confirmed';
    public const CANCELLED = 'Cancelled';
    public const RESOLVED  = 'Resolved';
    public const DONE      = 'Done';
    public const CLOSED    = 'Closed';
    public const RECEIVED  = 'ServiceOnRoad';
    public const SUSPENDED = 'Suspended';

    public const EVENT_CREATED = 'TicketCreated';
    public const EVENT_CHANGED = 'TicketChanged';


    /**
     * Filter list params by default
     * @var bool
     */
    protected static $_filterListParams = true;


    /**
     * Force load data from db
     * @var bool
     */
    protected $_dontUseCache = false;


    /**
     * Additional searchable fields
     * @var array
     */
    protected static $_searchableFields = [];


    /**
     * Additional search expressions
     * @var array
     */
    protected static $_searchExpressions = [];


    /**
     * @var string
     */
    public static $matrixClass = null;


    /**
     * Email message
     * @var Message
     */
    protected $_email;


    /**
     * Current post related with ticket create/update event
     * @var Post
     */
    protected $_currentPost;


    /**
     * Returns related sql table
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.ticket';
    }


    /**
     * Returns datasource for listing function
     * @return string
     */
    protected function _getListDatasource()
    {
        return 'app.ticket t';
    }


    /**
     * {@inheritDoc}
     */
    protected function _getQueryFields($context)
    {
        $fields = [
            'list' => [
                'ticket_id',
                'title',
                'ticket_type_id',
                'date_added',
                'ticket_status_id',
            ],
            'api-index' => [
                'ticket_id',
                'ticket_type_id',
            ],
            'link-list' => [
                'ticket_id',
                'ticket_type_id',
                'title',
                'ticket_status_id',
                'company_id',
                'owner_user_id',
                'responsible_user_id',
            ],
        ];

        if ($this->_hasField('update_date')) {
            $fields['api-index'][] = 'update_date';
        }

        $fields['index'] = $fields['list'];

        return $fields[$context] ?? [];
    }


    /**
     * Returns confirmed complete date
     * @return string
     */
    public function getConfirmedDate()
    {
        return $this->date_confirmed;
    }


    /**
     * Returns ticket description
     * @return string
     */
    public function getDesc()
    {
        return trim($this->description);
    }


    /**
     * @param int $postMaxLimit
     * @description https://support.velistech.com/zgloszenia/72058?selectTab=history
     * @return void
     */
    public static function setPostMaxLimit(int $postMaxLimit = 1000): void
    {
        self::$maxLimit = $postMaxLimit;
    }

    /**
     * {@inheritDoc}
     */
    public static function instance($objectId, $fields = null, $listDatasource = false)
    {
        $obj = new static();

        if (!empty($fields) && !is_array($fields)) {
            $fields = $obj->_getQueryFields($fields);
        }

        $instance = parent::instance($objectId, $listDatasource, $fields);
        if (is_array($instance)) {
            foreach ($instance as $key => $object) {
                $instance[$key] = Factory::create($object);
            }
            return $instance;
        } elseif ($instance) {
            return Factory::create($instance);
        } else {
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    public static function bufferedInstance($objectId, bool $refresh = false)
    {
        $result = parent::bufferedInstance($objectId, $refresh);
        if (!$result) {
            return $result;
        }

        $isArray = is_array($result);
        if (!$isArray) {
            $result = [$result];
        }

        foreach ($result as &$object) {
            $object = Factory::create($object);
        }

        return $isArray ? $result : $result[0];
    }

    /**
     * Returns available ticket types
     * @return array
     */
    public static function getTypes()
    {
        return Type::listCached();
    }


    /**
     * Ticket type name
     * @return Type
     * @throws InvalidArgumentException
     */
    public function getType()
    {
        return Type::get($this->ticket_type_id);
    }


    /**
     * Returns current direction description
     * @return string
     */
    public function getCurrentDirection()
    {
        if ($this->direction == '>') {
            return Lang::get('TICKET_TO_RESPONSIBLE');
        } else {
            return Lang::get('TICKET_TO_ISSUER');
        }
    }


    /**
     * Returns ticket title
     *
     * @param int $cut (cut to size)
     * @return string
     */
    public function getTitle($cut = null)
    {
        if ($cut) {
            return Output::cut($this->title, $cut);
        } else {
            return $this->title;
        }
    }


    /**
     * Base get ticket no method
     * @return int
     */
    public function getTicketNo()
    {
        return $this->id();
    }


    /**
     * Returns category instance
     * @return Category
     * @throws InvalidArgumentException
     */
    public function getCategory()
    {
        return Category::get($this['ticket_category_id']);
    }


    /**
     * Returns description text
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }


    /**
     * {@inheritDoc}
     * @return Log
     * @deprecated use addSimple() instead
     */
    public function add($updateObjectId = true)
    {
        $this->addSimple($updateObjectId);

        return $this->getFirstLog();
    }

    /**
     * @param bool $updateObjectId
     * @return $this
     * @throws ReflectionException
     * @todo rename this method to add() after removing all usages of old add() method
     */
    public function addSimple(bool $updateObjectId = false)
    {
        $commit = self::$_db->startTrans();

        try {
            if (!$this->getOwner()) {
                $this->setOwner(App::$user->id());
            }

            if (!$this['added_by_user_id']) {
                $this['added_by_user_id'] = App::$user->id();
            }

            $this->validate();

            Event::fireBefore(self::EVENT_CREATED, $this);

            $params = $this->getArrayCopy();

            $result = self::$_db->execFunction('ticket_api.create_ticket', $params);
            $this->_setId($result);

            if ($this->hasTriggers('add')) {
                self::$_db->onCommit(array($this, 'triggerAdd'));
            }

            Event::fire(self::EVENT_CREATED, $this);

            if ($commit) {
                self::$_db->commit();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }

        return $this;
    }

    /**
     * @throws BusinessLogicException
     */
    private function validate(): void
    {
        if (in_array($this['ticket_type_id'], ['Project', 'Bid'])) {
            return;
        }
        if (empty($this['responsible_user_id']) && empty($this['responsible_department_id'])) {
            throw new BusinessLogicException(Lang::get('GENERAL_RESPONSIBLE_PERSON_NOT_SELECTED'));
        }
    }


    /**
     * {@inheritDoc}
     */
    public function modify($checkDiff = false)
    {
        $disableEvents = false;

        if (func_num_args() > 1 && func_get_arg(1)) {
            $disableEvents = func_get_arg(1);
        }

        if (App::hasModule('Workflow') && !$disableEvents) {
            $restore = App::$di['db']->checkDuplicatedQueries(false);

            $current = $this->getCurrentInstance();
            Event::fireBefore(self::EVENT_CHANGED, $this, $current);

            $this->on(
                'modify',
                function ($ticket) use ($current) {
                    Event::fire(self::EVENT_CHANGED, $ticket, $current);
                }
            );

            App::$di['db']->checkDuplicatedQueries($restore);
        }

        return parent::modify($checkDiff);
    }


    /**
     * Builds list visibility conditions
     *
     * @param array|ArrayObject $params
     * @param array $visibilityRestricted
     */
    private static function _buildVisibilityConditions($params, $visibilityRestricted)
    {
        $visibilityConditions = [];

        // visibility restrictions for logged user (if not internally forced to show any ticket)
        if (App::$user->isLogged() && !(App::$registry['forceVisibility'] ?? false)) {
            $userDepartment = self::executeNestedQuery(fn() => App::$user->getDepartment());

            $instance = new static();

            $observerColumnName = self::getObserverColumnName();

            $visibilityConditions[] = "owner_user_id=:user_id";
            $visibilityConditions[] = "responsible_user_id=:user_id";
            $visibilityConditions[] = "EXISTS (
                SELECT 1 FROM app.ticket_observer_tab o
                WHERE t.ticket_id = o.ticket_id
                    AND o.$observerColumnName = :user_id
            )";

            if (in_array('responsible_department_id', $instance->_getListFields())) {
                $visibilityConditions[] = "responsible_department_id = :user_department_id";
            }

            if ($params['show_my_department_cases'] && (in_array('responsible_user_department_id', $instance->_getListFields()))) {
                $visibilityConditions[] = "responsible_user_department_id = :user_department_id";
            }

            if (App::$user->isSubstitute()) {
                $visibilityConditions[] =  "responsible_user_id IN (" . implode(',', App::$user->getSubstitutedUserIds()) . ")";
            }

            if ($params['partner_company_id'] && in_array('subcontractor_partner_id', $instance->_getListFields())) {
                $visibilityConditions[] = "(t.partner_id = t.subcontractor_partner_id AND t.subcontractor_partner_id = :partner_company_id)";
                self::$_listParams['partner_company_id'] = $params['partner_company_id'];
            }

            if (static::_getExtendedVisibilityConditions($params)) {
                $visibilityConditions[] = static::_getExtendedVisibilityConditions($params);
            }

            if (
                $params['hide_other_people_cases'] ||
                (App::$user->settings('HideOtherPeopleCases') &&
                !$params['show_other_people_cases'] &&
                !$params['show_to_accept'])
            ) {
                self::$_listConditions[] = "(" . implode(" OR ", $visibilityConditions) . ")";
                self::$_listParams['user_id'] = App::$user->id();
                if (in_array('responsible_department_id', $instance->_getListFields())) {
                    self::$_listParams['user_department_id'] = $userDepartment['main_department_id'] ?? $userDepartment['department_id'];
                }
            } elseif ($visibilityRestricted) {
                self::$_listConditions[] = "(
                    ticket_type_id NOT IN('" . implode("','", $visibilityRestricted) . "')
                    OR (" . implode(" OR ", $visibilityConditions) . ")
                )";

                self::$_listParams['user_id'] = App::$user->id();
                if (in_array('responsible_department_id', $instance->_getListFields())) {
                    self::$_listParams['user_department_id'] = $userDepartment['main_department_id'] ?? $userDepartment['department_id'];
                }
            }
        }
    }

    /**
     * Builds company restriction conditions list
     * @param array $additionalConditions
     * @param ParameterBag|null $params
     */
    protected static function _buildCompanyConditions(array $additionalConditions = [], ParameterBag $params = null)
    {
        if (App::$user->hasCompany()) {
            $observerColumnName = self::getObserverColumnName();

            $companyConditions[] = "owner_user_id = :user_id";
            $companyConditions[] = "responsible_user_id = :user_id";
            $companyConditions[] = "ticket_id IN (
                SELECT ticket_id FROM app.ticket_observer_tab o
                WHERE o.$observerColumnName = :user_id
            )";

            $instance = new static();
            $fields   = $instance->_getListFields();

            $companyInstance = new Company();
            $companyKey = str_replace('_id', '', $companyInstance->_getPrimaryKeyField());
            $shouldAddCompanyParams = !self::$_listParams['user_company_id'] instanceof AnyValue;

            if ($shouldAddCompanyParams) {
                if (in_array($companyKey . '_visible', $fields)) {
                    $companyConditions[] =  '(t.' . $companyKey . '_visible=1 AND t.' . $companyKey . '_id = :user_company_id)';
                } else {
                    $companyConditions[] = 't.' . $companyKey . '_id = :user_company_id';
                }
                self::$_listParams['user_company_id'] = App::$user->getCompanyId();
            }

            if (!$shouldAddCompanyParams) {
                unset(self::$_listParams['user_company_id']);
                $companyConditions[] = "t.{$companyKey}_id IS NOT NULL";
            }


            if (in_array('owner_in_project', $fields)) {
                $projectCondition = "(t.owner_in_project = 1";
                if (in_array($companyKey . '_visible', $fields)) {
                    $projectCondition .= " AND " . $companyKey . "_visible=1";
                }
                $projectCondition .= " AND EXISTS (
                    SELECT 1 FROM app.project_" . $companyKey . "_tab pc
                    WHERE pc.project_id  = t.project_id
                      AND pc." . $companyKey . "_id = :user_company_id
                ))";
                self::$_listParams['user_company_id'] = App::$user->getCompanyId();
                $companyConditions[] = $projectCondition;
            }

            $companyConditions  = array_merge(
                $companyConditions,
                $additionalConditions
            );

            self::$_listConditions[] = "(" . implode(" OR ", $companyConditions) . ")";

            self::$_listParams['user_id'] = App::$user->id();
        }
    }


    /**
     * Builds salesman restriction conditions list
     * @param array<string, mixed>|ArrayObject<string, mixed> $params
     * @return string|bool
     */
    protected static function _getExtendedVisibilityConditions($params)
    {
        return false;
    }


    /**
     * {@inheritDoc}
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        $params = new ParameterBag($params);

        if (!self::$_openedStatuses) {
            $openStatuses = Status::getOpenStatuses(true);
        } else {
            $openStatuses = self::$_openedStatuses;
        }

        if ($params['ticket_category_id'] && !is_array($params['ticket_category_id'])) {
            $category = new Category($params['ticket_category_id']);
            $categories = [$category->id()];
            if ($category->getChildrenIds()) {
                $categories = array_merge($categories, $category->getChildrenIds(true));
            }

            $params['ticket_category_id'] = $categories;
        }
        $visibilityRestricted = [];

        // tickets' specific conditions
        $searchExpressions = [];
        $searchableFields = [];
        $queryFields = [];

        foreach (Factory::getRegisteredSpecs() as $spec => $class) {
            if (isset($params['ticket_type_id'])) {
                if (is_array($params['ticket_type_id']) && !in_array($spec, $params['ticket_type_id'])) {
                    continue;
                } elseif (is_string($params['ticket_type_id']) && $spec != $params['ticket_type_id']) {
                    continue;
                }
            }

            $class::_setSpecConditions($params);
            $searchableFields += $class::$_searchableFields;
            $searchExpressions += $class::$_searchExpressions;

            if (!empty($fields) && !is_array($fields)) {
                /** @var Ticket $obj */
                $obj = new $class();
                $queryFields += $obj->_getQueryFields($fields);
            }

            if (App::$user->isLogged()) {
                if (!App::$user->hasPriv($class::$_fullVisibilityPriv)) {
                    $visibilityRestricted[] = $spec;
                }
            }
        }

        if (!empty($fields) && !is_array($fields)) {
            $fields = array_unique($queryFields);
        }

        self::_buildVisibilityConditions($params, $visibilityRestricted);
        static::_buildCompanyConditions([], $params);

        if ($params['search']) {
            if (strpos($params['search'], '#') === 0) {
                self::$_listConditions[] = "ticket_id=:ticket_id";
                self::$_listParams['ticket_id'] = Filter::filterInt(substr($params['search'], 1));
            } else {
                $searchTab = explode(' ', trim($params['search']));

                foreach ($searchTab as $key => $word) {
                    $word = str_replace('\\', '\\\\', $word);

                    $conditionTemp = [];
                    $conditionTemp[] = "title ILIKE :search_$key";
                    $conditionTemp[] = "description ILIKE :search_$key";

                    if (self::hasField('company_name', true)) {
                        $conditionTemp[] = "company_name ILIKE :search_$key";
                    }

                    foreach (array_unique($searchableFields) as $field) {
                        $conditionTemp[] = "$field ILIKE :search_$key";
                    }

                    foreach (array_unique($searchExpressions) as $expression) {
                        $conditionTemp[] = strtr($expression, ['{search_key}' => ":search_$key"]);
                    }

                    if (strpos($word, '_')) {
                        $ticketTitle = explode('_', $word);
                        if (is_numeric(reset($ticketTitle))) {
                            $conditionTemp[] = "ticket_id::varchar LIKE :search_exact_$key";

                            if (self::hasField('prime_contractor_ticket_no', true)) {
                                $conditionTemp[] = "prime_contractor_ticket_no::varchar LIKE :search_exact_$key";
                            }

                            self::$_listParams["search_exact_$key"] = reset($ticketTitle);
                        }
                    }

                    if (Filter::validateDigits(trim($word))) {
                        $conditionTemp[] = "ticket_id::varchar LIKE :search_exact_$key";

                        if (self::hasField('prime_contractor_ticket_no', true)) {
                            $conditionTemp[] = "prime_contractor_ticket_no::varchar LIKE :search_exact_$key";
                        }

                        self::$_listParams["search_exact_$key"] = $word;
                    }

                    if ($params['search_in_posts']) {
                        $condition = "EXISTS (SELECT 1
                                                    FROM app.ticket_post_tab tp
                                                    WHERE tp.ticket_id = t.ticket_id";

                        if (App::$user->hasCompany()) {
                            $condition .= "               AND ticket_post_visibility_id = 'Public'";
                        }

                        $condition .= "                   AND content ILIKE :search_$key)";

                        $conditionTemp[] = $condition;
                    }

                    self::$_listConditions[] = "(" . implode(' OR ', $conditionTemp) . ")";

                    self::$_listParams["search_$key"] = '%' . trim($word, '%') . '%';
                }
            }
        }

        if ($params['added_from'] && Filter::validateDate($params['added_from'])) {
            self::$_listConditions[] = "date_trunc('day', date_added) >= :added_from";
            self::$_listParams['added_from'] = $params['added_from'];
        }

        if ($params['added_to'] && Filter::validateDate($params['added_to'])) {
            self::$_listConditions[] = "date_trunc('day',date_added) <= :added_to";
            self::$_listParams['added_to'] = $params['added_to'];
        }

        if ($params['confirmed_from'] && Filter::validateDate($params['confirmed_from'])) {
            self::$_listConditions[] = "date_confirmed >= :confirmed_from";
            self::$_listParams['confirmed_from'] = $params['confirmed_from'];
        }

        if ($params['confirmed_to'] && Filter::validateDate($params['confirmed_to'])) {
            self::$_listConditions[] = "date_confirmed <= :confirmed_to";
            self::$_listParams['confirmed_to'] = $params['confirmed_to'];
        }

        if ($params['end_from'] && Filter::validateDate($params['end_from'])) {
            self::$_listConditions[] = "date_end >= :end_from";
            self::$_listParams['end_from'] = $params['end_from'];
        }

        if ($params['end_to'] && Filter::validateDate($params['end_to'])) {
            self::$_listConditions[] = "date_end <= :end_to";
            self::$_listParams['end_to'] = $params['end_to'];
        }

        if ($params['last_modified_from'] && Filter::validateDate($params['last_modified_from'])) {
            self::$_listConditions[] = "last_modified_date >= :last_modified_from";
            self::$_listParams['last_modified_from'] = $params['last_modified_from'];
        }

        if ($params['last_modified_to'] && Filter::validateDate($params['last_modified_to'])) {
            self::$_listConditions[] = "last_modified_date < :last_modified_to";
            self::$_listParams['last_modified_to'] = $params['last_modified_to'];
        }

        foreach (['Closed', 'Done'] as $status) {
            $fieldFrom = mb_strtolower($status) . '_from';
            $fieldTo = mb_strtolower($status) . '_to';

            $logActionId = $status;
            if ($status === 'Closed') {
                $logActionId = 'Close';
            }

            if ($params[$fieldFrom] && Filter::validateDate($params[$fieldFrom])) {
                self::$_listConditions[] = "ticket_status_id='{$status}'";
                self::$_listConditions[] = "(
                    SELECT date_trunc('day', max(tl.date_modified)) FROM app.ticket_log_tab tl
                    WHERE tl.ticket_status_id = '{$status}'
                      AND tl.ticket_log_action_id = '{$logActionId}'
                      AND tl.ticket_id=t.ticket_id
                ) >= :{$fieldFrom}";
                self::$_listParams[$fieldFrom] = $params[$fieldFrom];
            }

            if ($params[$fieldTo] && Filter::validateDate($params[$fieldTo])) {
                self::$_listConditions[] = "ticket_status_id='{$status}'";
                self::$_listConditions[] = "(
                    SELECT date_trunc('day', max(tl.date_modified)) FROM app.ticket_log_tab tl
                    WHERE tl.ticket_status_id = '{$status}'
                      AND tl.ticket_log_action_id = '{$logActionId}'
                      AND tl.ticket_id = t.ticket_id
                ) <= :{$fieldTo}";
                self::$_listParams[$fieldTo] = $params[$fieldTo];
            }
        }

        if ($params['delayed']) {
            self::$_listConditions[] = "end_date < NOW()";
        }

        if (!$params['show_finished']) {
            self::$_listConditions[] = "ticket_status_id IN('" . implode("', '", $openStatuses) . "')";
        }

        if (!$params['show_finished'] && App::$user->settings('HideOwnerPendingCases') && !$params['show_owner_pending']) {
            self::$_listConditions[] = "(owner_user_id = :user_id OR direction = '>' OR ticket_type_id = 'Project')";
            self::$_listParams['user_id'] = App::$user->id();
        }

        if ($params['ticket_status_id'] && is_array($params['ticket_status_id'])) {
            $params['ticket_status_id'] = array_filter($params['ticket_status_id']);
        }

        if ($params['ticket_classification_id'] && is_array($params['ticket_classification_id'])) {
            $params['ticket_classification_id'] = array_filter($params['ticket_classification_id']);
        }

        if ($params['excluded_ticket_id']) {
            if (!is_array($params['excluded_ticket_id'])) {
                self::$_listConditions[] = 'ticket_id != :excluded_ticket_id';
                self::$_listParams['excluded_ticket_id'] = $params['excluded_ticket_id'];
            } else {
                $excludedTicketIds = Filter::filterInts($params['excluded_ticket_id']);
                if (count($excludedTicketIds)) {
                    self::$_listConditions[] = 'ticket_id NOT IN(' . implode(',', $excludedTicketIds) . ')';
                }
            }
        }

        $tickets = [];

        foreach (parent::getList($page, $params, $order, $limit, $fields) as $ticketId => $ticket) {
            $tickets[$ticketId] = Factory::create($ticket);
        }

        return $tickets;
    }


    /**
     * Override in spec implementation to add specific params handling
     * @param array|ArrayObject $params
     */
    protected static function _setSpecConditions(&$params)
    {
    }


    /**
     * Returns true if ticket confirmed
     * @return bool
     */
    public function isConfirmed()
    {
        return $this->date_confirmed != null;
    }


    /**
     * Returns true if ticket is delayed
     * @return bool
     */
    public function isDelayed()
    {
        return substr($this->date_confirmed, 0, 16) < date('Y-m-d H:i');
    }


    /**
     * Reopens ticket
     *
     * @param string $post
     * @param string $newStatusId
     * @param string $postVisibility
     * @param array $additionalParams
     * @return Log|bool
     * @throws BusinessLogicException
     */
    public function reopen($post = null, $newStatusId = null, $postVisibility = Post::TYPE_PUBLIC, array $additionalParams = null)
    {
        if (!$this->isFinished()) {
            throw new BusinessLogicException(Lang::get('TICKET_TICKET_NOT_CLOSED'));
        }

        $commit = self::$_db->startTrans();

        try {
            $params = [
                'ticket_id' => $this->id(),
                'ticket_status_id' => $newStatusId,
            ];

            if ($post instanceof Post) {
                $params['post_visibility'] = $post['ticket_post_visibility_id'] ?: $postVisibility;
                if (!App::$user->isLogged()) {
                    $params['user_id'] = $post->getAuthor()->id();
                }
            } else {
                $params['post']             = $post;
                $params['post_visibility']  = $postVisibility;
                $params['user_id']          = App::$user->id();
            }

            $params['person_id'] = ($post instanceof Post && $post['person_id']) ? $post['person_id'] : $params['user_id'];

            if (!$params['user_id']) {
                $params['user_id'] = $post['user_id'] ?: App::$user->id();
            }

            if ($additionalParams) {
                $params += $additionalParams;
            }

            // ticket_api.reopen by default return postId but we here need logId
            if (!isset($params['return_log_id'])) {
                $params['return_log_id'] = 1;
            }

            $logId = self::$_db->execFunction('ticket_api.reopen', $params);

            if ($post instanceof Post) {
                if ($post->id()) {
                    $post->connectLogs(array($logId));
                }
            }

            if ($commit) {
                self::$_db->commit();
            }

            if ($logId) {
                $log = Arrays::getFirst(Log::listAll([
                    'ticket_log_id' => $logId,
                    'ticket_id' => $this->id(),
                ]));

                if ($log) {
                    $log->setNotificationVisibility($postVisibility);
                    $log->setTicket($this);

                    return $log;
                }
            }

            return false;
        } catch (Exception $ex) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $ex;
        }
    }


    /**
     * Returns list of defined classification types
     * @return array
     */
    public static function getClassificationTypes()
    {
        return Dictionary::get('app.ticket_classification_tab');
    }


    /**
     * Returns ticket url
     * @return string
     */
    public function getUrl()
    {
        return App::getBaseUrl() . $this->url();
    }


    /**
     * Returns classification name
     * @return Classification|null
     * @throws InvalidArgumentException
     */
    public function getClassification()
    {
        return Classification::get($this->ticket_classification_id);
    }


    /**
     * Returns list of defined priority types
     * @return array
     */
    public static function getPriorityTypes()
    {
        return Dictionary::get('app.ticket_priority_tab');
    }


    /**
     * Returns ticket priority name
     * @return Priority
     * @throws InvalidArgumentException
     */
    public function getPriority()
    {
        return Priority::get($this->ticket_priority_id);
    }


    /**
     * Returns ticket priority color
     * @return string
     * @throws VelisException
     */
    public function getPriorityColor()
    {
        $priorities = Priority::listCached();
        $priority = Arrays::getColumn($priorities, 'color', 'ticket_priority_id');
        return $priority[$this->ticket_priority_id];
    }


    /**
     * Returns related email message
     * @return Message|void
     */
    public function getEmail()
    {
        if ($this->email_message_id) {
            if (!$this->_email) {
                $this->_email = Message::instance($this->email_message_id);
            }

            return $this->_email;
        }
    }


    /**
     * Set email
     * @param $email
     * @return Ticket
     */
    public function setEmail($email)
    {
        $this->_email = $email;

        return $this;
    }


    /**
     * Sets current post related with add/update event
     *
     * @param Post $post
     * @return Ticket
     * @throws VelisException
     */
    public function setCurrentPost(Post $post)
    {
        if ($post === null || $post instanceof Post) {
            $this->_currentPost = $post;
        } else {
            // soft error on production environment
            VelisException::raise(
                'Ticket::setCurrentPost() require post instance or null to be passed'
            );
        }

        return $this;
    }


    /**
     * Returns current post instance related with add/update event
     * @return Post
     */
    public function getCurrentPost()
    {
        return $this->_currentPost;
    }


    /**
     * Returns schedule events count
     * @return int
     */
    public function hasScheduleEvents()
    {
        return self::$_db->getOne(
            "SELECT COUNT(*) FROM app.ticket_schedule_tab WHERE ticket_id=:id",
            ['id' => $this->id()]
        );
    }


    /**
     * @param string $type
     * @return Notification
     */
    public function createNotification($type)
    {
        return new Notification($this, $type);
    }


    /**
     * @param Label $label
     */
    public function onLabelAdd(Label $label)
    {
        // do nothing; this method may be overridden in child classes
    }

    /**
     * Checks whether the user can remove the observer
     *
     * @param User $observer
     * @param User|null $user
     *
     * @return bool
     */
    public function canRemoveObserver($observer, $user = null)
    {
        $service = App::$di->get('ticketService');
        return $service->canRemoveObserver($this, $observer, $user);
    }


    /**
     * @return Ticket
     */
    public static function getStatic(): Ticket
    {
        return new static();
    }

    public function relationPostFiles(): HasManyThroughRelation
    {
        return $this->hasManyThrough(
            File::class,
            'app.ticket_post_tab',
            'ticket_id',
            'ticket_id',
            'ticket_post_id',
            'ticket_post_id'
        );
    }

    /**
     * @return File[]
     */
    public function getPostFiles(): array
    {
        try {
            $files = $this->getRelation('postFiles');
        } catch (NotLoadedRelationException $e) {
            $files = $this->relationPostFiles()
                ->get();
        }

        return $files ?? [];
    }

    /**
     * @return HasManyThroughRelation<User, static>
     */
    public function relationObservers(): HasManyThroughRelation
    {
        /** @var class-string<User> $userClass */
        $userClass = App::$config->settings->userClass ?? User::class;

        return $this->hasManyThrough($userClass, 'app.ticket_observer_tab', 'ticket_id', 'ticket_id', 'user_id', 'user_id');
    }

    /**
     * @return HasOneRelation<User, static>
     */
    public function relationCoordinator(): HasOneRelation
    {
        /** @var class-string<User> $userClass */
        $userClass = App::$config->settings->userClass ?? User::class;

        return $this->hasOne($userClass, 'coordinator_user_id', 'user_id');
    }

    public function isCancelled(): bool
    {
        return $this->ticket_status_id === self::CANCELLED;
    }
}
