<?php

namespace Velis;

use Exception;
use InvalidArgumentException;
use ReflectionObject;
use User\User as SystemUser;
use Velis\Bpm\Schedule\Event as ScheduleEvent;
use Velis\Bpm\Ticket\Ticket;
use Velis\Bpm\Workflow\Event;
use Velis\Bpm\Workflow;
use Velis\Db\AnyValue;
use Velis\Db\Exception as DbException;
use Velis\Model\Cacheable;
use Velis\Model\DataObject;

/**
 * Object label model
 * @author Olek Procki <olo@velis.pl>
 */
class Label extends DataObject implements Cacheable
{
    public const TICKET = 'ticket';
    public const SCHEDULE = 'schedule';
    public const USER = 'user';

    /**
     * @var int
     */
    protected static $_favLabelId = 1;


    /**
     * @var string
     */
    protected static $_listDefaultOrder = 'name';


    /**
     * For public labels users who're sharing this label
     * @var User[]
     */
    protected $_sharingUsers;


    /**
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.label_tab';
    }


    /**
     * @return string
     */
    protected function _getListDatasource()
    {
        return 'app.label l';
    }


    /**
     * Returns label name
     * @return string
     */
    public function getName()
    {
        return $this->getTranslatedName();
    }


    /**
     * Returns label owner
     * @return SystemUser
     */
    public function getOwner()
    {
        return SystemUser::get($this->user_id);
    }

    /**
     * @param $params
     * @return string|null
     */
    protected static function getExtendedVisibilityConditions()
    {
        return null;
    }

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

        if ($params['for'] && !App::$user->hasPriv('Admin', 'LabelViewAll')) {
            $condition = "(user_id=:for_user_id OR user_id IN (
                SELECT u.user_id FROM acl.user u WHERE u.substitute_user_id = :for_user_id
            )";

            if (self::hasField('company_id') && App::$user->hasCompany()) {
                $condition .= " OR company_id=" . App::$user->getCompanyId();
            } elseif (App::$config->settings->allPublicLabelsAccess) {
                $condition .= " OR is_public=1";
            } else {
                $condition .= " OR EXISTS (
                    SELECT 1 FROM app.label_to_user_tab ltu
                    WHERE ltu.label_id = l.label_id AND (ltu.user_id = :for_user_id OR ltu.user_id IN (
                        SELECT u.user_id FROM acl.user u WHERE u.substitute_user_id = :for_user_id
                    ))
                )";

                if (static::getExtendedVisibilityConditions()) {
                    $condition .= " OR " . static::getExtendedVisibilityConditions();
                }

                if (self::hasField('label_type_id')) {
                    $condition .= " OR l.label_type_id = 'user'";
                }
            }
            $condition .= ")";

            self::$_listParams['for_user_id'] = $params['for'];
            self::$_listConditions[] = $condition;
        }

        if ($params['has_name']) {
            self::$_listConditions[] = '("name").' . $params['has_name'] . " <> ''";
        }

        return parent::getList($page, $params, $order, $limit, $fields);
    }


    /**
     * Set additional users for label
     *
     * @param User[]|int[] $users
     * @return Label
     * @throws Exception
     */
    public function setUsers($users)
    {
        $commit = self::$_db->startTrans();

        try {
            $this->unsetUsers();

            if (is_array($users)) {
                foreach ($users as $user) {
                    $userId = $user instanceof User ? $user->id() : $user;

                    self::$_db->insert(
                        'app.label_to_user_tab',
                        array(
                            'label_id' => $this->id(),
                            'user_id'  => $userId
                        )
                    );

                    unset(App::$cache[self::getCacheName($userId)]);
                }
                if ($commit) {
                    self::$_db->commit();
                }
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }
        return $this;
    }


    /**
     * Unset users for label
     * @return Label
     * @throws \Velis\Exception
     */
    public function unsetUsers()
    {
        $existingUsers = Arrays::getColumn(
            self::$_db->getAll(
                "SELECT user_id FROM app.label_to_user_tab WHERE label_id= :id",
                array('id' => $this->id())
            ),
            'user_id'
        );

        if ((App::$config->settings->allPublicLabelsAccess && $this['is_public']) || (self::hasField('label_type_id') && self::USER === $this->label_type_id)) {
            $privilegedUsers = Arrays::getColumn(
                Acl::getPriviledgedUsers('Admin', 'LabelAccess', ['user_id']),
                'user_id'
            );
        } else {
            $privilegedUsers = Arrays::getColumn(
                Acl::getPriviledgedUsers('Admin', 'LabelViewAll', ['user_id']),
                'user_id'
            );
        }

        $users = array_merge($privilegedUsers, $existingUsers);

        foreach ($users as $userId) {
            unset(App::$cache[self::getCacheName($userId)]);
        }

        self::$_db->execDML(
            "DELETE FROM app.label_to_user_tab WHERE label_id=:id",
            array('id' => $this->id())
        );

        return $this;
    }


    /**
     * Adds new label
     *
     * @param User[]|int[] $users
     * @return Label
     * @throws Exception
     */
    public function add($users = null)
    {
        $commit = self::$_db->startTrans();

        try {
            parent::add(true);
            unset(App::$cache[self::getCacheName($this->user_id)]);

            if ($this['is_public'] && $this['label_type_id'] != self::USER) {
                $this->setUsers($users);
            }

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

            throw $e;
        }

        return $this;
    }


    /**
     * Modifies label
     * @param User[]|int[] $users
     * @return Label
     *
     * @throws Exception
     */
    public function modify($users = null)
    {
        $commit = self::$_db->startTrans();

        try {
            parent::modify();
            unset(App::$cache[self::getCacheName($this->user_id)]);

            if ($this->is_public && $this['label_type_id'] != self::USER) {
                $this->setUsers($users);
            } else {
                $this->unsetUsers();
            }

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

        return $this;
    }


    /**
     * Saves label
     *
     * @param User[]|int[] $users
     * @return Label
     * @throws Exception
     */
    public function save($users = null)
    {
        if ($this->id()) {
            return $this->modify($users);
        } else {
            return $this->add($users);
        }
    }

    /**
     * Returns sharing users
     *
     * @param bool $idOnly
     * @return User[]|int[]
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function getSharingUsers(bool $idOnly = false): array
    {
        if ($idOnly) {
            return $this->getSharingUserIds();
        }

        if (!isset($this->_sharingUsers)) {
            $userClass = App::$config->settings->userClass ?: User::class;
            $this->_sharingUsers = $userClass::bufferedInstance($this->getSharingUserIds());
        }

        return $this->_sharingUsers;
    }

    /**
     * @return int[]
     */
    public function getSharingUserIds(): array
    {
        if (!isset($this['sharing_users'])) {
            $this->append(self::bufferedInstance($this->id()));
        }

        if (empty($this['sharing_users'])) {
            return [];
        }

        return Filter::filterInts(explode(',', $this['sharing_users']));
    }

    /**
     * Returns favourite label ID
     * @return string
     */
    public static function favLabelId()
    {
        return self::$_favLabelId;
    }

    /**
     * Check if the user (defaults to current) can edit all labels
     * @param User $user
     * @return bool
     */
    public static function canEditAll($user = null): bool
    {
        $user ??= App::$user;
        $privSetting = App::$config->settings->labelEditAllPriv;
        return $privSetting && $user->hasPriv(Arrays::toArray($privSetting));
    }


    /**
     * Returns type for object
     *
     * @param DataObject $object
     * @return string|void
     *
     * @throws InvalidArgumentException
     */
    public static function getTypeForObject($object)
    {
        if (!($object instanceof DataObject)) {
            throw new InvalidArgumentException('Must provide label type or full object!');
        }
        if ($object instanceof Ticket) {
            return self::TICKET;
        } elseif ($object instanceof ScheduleEvent) {
            return self::SCHEDULE;
        } elseif ($object instanceof SystemUser) {
            return self::USER;
        } else {
            if (App::$config->label) {
                foreach (Arrays::toArray(App::$config->label) as $labelType => $class) {
                    if ($object instanceof $class) {
                        return $labelType;
                    }
                }
            }
        }
    }


    /**
     * Return types of labels
     *
     * @return array
     */
    public static function getTypes()
    {
        $types = array();

        if (App::$config->label) {
            foreach (Arrays::toArray(App::$config->label) as $labelType => $class) {
                $types[] = $labelType;
            }
        }
        $refObj = new ReflectionObject(new self());
        $types = array_merge($types, $refObj->getConstants());

        return $types;
    }

    /**
     * Validates object type
     *
     * @param string $type
     * @throws InvalidArgumentException
     */
    protected function checkType($type)
    {
        if (!$type || !in_array($type, self::getTypes())) {
            throw new InvalidArgumentException('Unsupported object for label or type not provided!');
        }
    }


    /**
     * Marks object with label
     *
     * @param DataObject|int $object
     * @param string|null $type
     * @param bool $fireEvent
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws Exception
     */
    public function mark($object, $type = null, $fireEvent = true)
    {
        if ($type == null) {
            $type = self::getTypeForObject($object);
        }
        $this->checkType($type);

        $relTable = 'app.label_' . $type . '_tab';
        $params = $this->getParams($object, $type);

        try {
            $restore = self::$_db->checkDuplicatedQueries(false);
            $inserted = self::$_db->getOne('
                INSERT INTO ' . $relTable . ' (' . implode(',', array_keys($params)) . ')
                VALUES(:' . implode(',:', array_keys($params)) . ')
                ON CONFLICT DO NOTHING
                RETURNING 1
            ', $params);
            self::$_db->checkDuplicatedQueries($restore);

            if (!$inserted) {
                return;
            }

            if (self::TICKET == $type) {
                if (!$object instanceof Ticket) {
                    $object = Ticket::instance($object);
                }

                $object->onLabelAdd($this);
            }

            if (is_object($object)) {
                $object->_labels[] = self::instance($params['label_id']);
            }

            if (App::hasModule('Workflow') && $fireEvent) {
                $subject = $this->getSubject($object, $type);

                if ($subject) {
                    Event::fire(ucfirst($type) . 'LabelAdded', $subject, $this);
                }
            }
        } catch (Exception $e) {
            if ($e->getCode() != DbException::CODE_DUPLICATED) {
                throw $e;
            }
        }
    }


    /**
     * @param int    $object
     * @param string $type
     * @return mixed
     */
    private function getSubject($object, $type)
    {
        $subject = null;

        if (!$object instanceof DataObject) {
            if ($type == self::TICKET) {
                $subject = Ticket::bufferedInstance($object);
            } elseif ($type == self::USER) {
                $subject = SystemUser::bufferedInstance($object);
            } elseif (App::$config->label->{$type}) {
                $subjectClass = App::$config->label->{$type};
                $subject = $subjectClass::instance($object);
            }
        } else {
            $subject = $object;
        }

        return $subject;
    }


    /**
     * Unsets object label
     *
     * @param DataObject|int $object
     * @param string         $type
     * @param bool           $fireEvent
     *
     * @return int current labels count of object for user
     */
    public function unmark($object, $type = null, $fireEvent = true)
    {
        static $already = [];

        if ($type == null) {
            $type = self::getTypeForObject($object);
        }
        $this->checkType($type);

        // this event should be fired before we remove label from object
        // because of the validation
        if (App::hasModule('Workflow') && $fireEvent) {
            $subject = $this->getSubject($object, $type);

            if ($subject) {
                Event::fire(ucfirst($type) . 'LabelRemoved', $subject, $this);
            }
        }

        $relTable = 'app.label_' . $type . '_tab';
        $params = $this->getParams($object, $type, true);
        $ownerUserField = $object instanceof SystemUser ? 'added_by_user_id' : 'user_id';

        $ID = $this->id();
        if (!$already[$ID]['is_public']) {
            $already[$ID]['is_public'] = $this->is_public;
        }

        $isPublic = $already[$ID]['is_public'];
        $sameCompany = self::hasField('company_id') && App::$user->hasCompany() && App::$user->getCompanyId() == $this->company_id;

        if ($isPublic || Workflow::isRunning() || $sameCompany) {
            unset($params[$ownerUserField]);
        }

        $restore = self::$_db->checkDuplicatedQueries(false);
        self::$_db->execDML(
            "DELETE FROM $relTable WHERE 1=1 " . self::$_db->conditions($params),
            $params
        );
        self::$_db->checkDuplicatedQueries($restore);

        if (!empty($object->_labels)) {
            $object->_labels = array_filter(
                $object->_labels,
                fn ($label) => $label['label_id'] != $params['label_id']
            );
        }

        unset($params['label_id']);

        if (!$already[$ID]['user_id']) {
            $already[$ID]['user_id'] = $this->user_id;
        }

        $userId = $already[$ID]['user_id'];

        if (!$userId) {
            return 0;
        } else {
            $params[$ownerUserField] = $userId;

            $restore = self::$_db->checkDuplicatedQueries(false);
            $tmp = self::$_db->getOne(
                "SELECT COUNT(*) FROM $relTable WHERE 1=1 " . self::$_db->conditions($params),
                $params
            );
            self::$_db->checkDuplicatedQueries($restore);

            return $tmp;
        }
    }


    /**
     * Builds params array for SQL
     *
     * @param DataObject|int $object
     * @param string $type
     * @return array
     */
    protected function getParams($object, $type, $unmarkAll = false)
    {
        $params = $this->_getPrimaryKeyParam();
        $ownerUserField = $object instanceof SystemUser ? 'added_by_user_id' : 'user_id';
        if (!$unmarkAll || !self::canEditAll()) {
            $params[$ownerUserField] = $object[$ownerUserField] ?? App::$user->id();
        }

        if (!($object instanceof DataObject) || !is_array($object->id())) {
            $params[$type . '_id'] = $object instanceof DataObject ? $object->id() : $object;
        } else {
            $params = $params + $object->id();
        }

        return $params;
    }


    /**
     * Removes label (also deletes assignments)
     *
     * @throws Exception
     */
    public function remove()
    {
        if (App::hasModule('Workflow')) {
            $sql = "SELECT 1 FROM app.workflow_action_tab WHERE workflow_action_type_id = 'TicketLabel' AND params::json->>'label_id' = :label_id::text LIMIT 1";
            if (self::$_db->getOne($sql, ['label_id' => $this->id()])) {
                throw new Exception(Lang::get('GENERAL_LABEL_USED_IN_WORKFLOW'));
            }
        }

        $commit = self::$_db->startTrans();
        try {
            if (self::canEditAll() && $this['label_type_id'] === self::TICKET) {
                // We do not have to fire event about removing the label from the ticket, because we already checked that
                //  there are no workflows that use this label.
                $this->unmark(new AnyValue(), self::TICKET, false);
            }

            $this->unsetUsers();
            parent::_remove();

            unset(App::$cache[self::getCacheName($this->user_id)]);

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


    public function getLabelByName($name)
    {
        $query = "
            SELECT
                label_id
            FROM  app.label_tab
            WHERE name = '" . $name . "'";

        return self::$_db->getOne($query);
    }


    /**
     * Gets label cache name
     * @param int|bool $userId
     * @return string
     */
    public static function getCacheName($userId = false)
    {
        if (self::hasField('company_id') && App::$user->hasCompany()) {
            return 'labelsCompany' . App::$user->getCompanyId();
        }
        $cacheName = 'labels' . $userId ?: App::$user->id();
        if (App::$config->settings->showLabelOnlyInUserLanguage) {
            $cacheName .= Lang::getLanguage();
        }
        return $cacheName;
    }

    /**
     * @param SystemUser $user
     * @return bool
     */
    public function checkAccess(SystemUser $user): bool
    {
        if ($user->hasPriv('Admin', 'LabelViewAll')) {
            return true;
        }

        if (App::$config->settings->allPublicLabelsAccess && $this->is_public) {
            return true;
        }

        $allowedUserIds = array_merge($this->getSharingUserIds(), [$this->getUserId()]);
        $substitutedUserIds = $user->getSubstitutedUserIds();
        $currentAndSubstitutedUserIds = array_merge($substitutedUserIds, [$user->id()]);

        if (array_intersect($allowedUserIds, $currentAndSubstitutedUserIds)) {
            return true;
        }

        // @todo move to child class in BPM
        if (self::hasField('company_id') && $this->company_id == $user->getCompanyId()) {
            return true;
        }

        return false;
    }

    /**
     * @return int
     */
    public function getUserId()
    {
        return $this->user_id;
    }
}
