<?php

namespace Velis\Bpm\Ticket\Mixin;

use LogicException;
use User\User;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Person;
use Velis\Bpm\Ticket\Exception\PersonAlreadyObserverException;
use Velis\Bpm\Ticket\Exception\PersonIsNotObserverException;
use Velis\Db\WithoutDuplicateQueryCheckTrait;
use Velis\Exception;
use Velis\Model\DataObject;
use Velis\PersonInterface;

/**
 * Observers functionality
 * @author Olek Procki <olo@velis.pl>
 */
trait ObserverTrait
{
    use WithoutDuplicateQueryCheckTrait;

    /**
     * Ticket observers
     * @var PersonInterface[]
     */
    protected $_observers;


    /**
     * Ticket observers ids
     * @var int[]
     */
    protected $_observerIds;


    /**
     * Returns ticket observers list
     * @return User[]|Person[]
     * @throws Exception
     */
    public function getObservers()
    {
        if (!is_array($this->_observers)) {
            $this->_observers = [];

            if ($observerIds = $this->getObserverIds()) {
                $userObservers = User::bufferedInstance($observerIds);
                $personObservers = [];

                // has any non-user observer
                if (count($observerIds) > count($userObservers)) {
                    $personObservers = static::listPersonById($observerIds);
                }

                foreach ($observerIds as $observerId) {
                    if (array_key_exists($observerId, $userObservers)) {
                        $this->_observers[$observerId] = $userObservers[$observerId];
                    } else {
                        $this->_observers[$observerId] = $personObservers[$observerId];
                    }
                }
            }
        }

        return $this->_observers;
    }


    /**
     * Returns list of persons by ids
     * @param int|int[] $personId
     * @return Person|Person[]|null
     */
    protected static function listPersonById($personId)
    {
        return Person::listById($personId);
    }


    /**
     * Returns ticket observers ids list
     * @return int[]
     * @throws Exception
     */
    public function getObserverIds(): array
    {
        if ($this->_observerIds === null) {
            if ($this->offsetExists('observers_ids')) {
                $result = [];
                $observerIds = $this->fieldToArray('observers_ids');
                array_walk($observerIds, function ($value) use (&$result) {
                    $result[$value] = [self::getObserverColumnName() => $value];
                });
            } else {
                $query = "SELECT " . self::getObserverColumnName() . " FROM app.ticket_observer_tab WHERE ticket_id=:ticket_id";
                $result = $this->withoutDuplicateQueryCheck(
                    fn () => self::$_db->getAll($query, ['ticket_id' => $this->id()])
                );
            }
            $this->_observerIds = [];

            if ($result) {
                if (Arrays::hasColumn($result, 'user_id')) {
                    $this->_observerIds = Arrays::getColumn($result, 'user_id');
                } elseif (Arrays::hasColumn($result, 'person_id')) {
                    $this->_observerIds = Arrays::getColumn($result, 'person_id');
                }
            }
        }

        return $this->_observerIds;
    }


    /**
     * Adds person to ticket observers
     *
     * @param PersonInterface|int $observer
     * @param bool $gracefully
     * @return int|null
     * @throws LogicException
     * @throws Exception
     * @throws PersonAlreadyObserverException
     */
    public function addObserver($observer, $gracefully = false)
    {
        if ($this->isObserver($observer)) {
            if ($gracefully) {
                return null;
            }

            throw new PersonAlreadyObserverException();
        }

        $observerId = $observer instanceof DataObject ? $observer->id() : $observer;

        if (isset($this->_observers[$observerId])) {
            return null;
        }

        $params = [
            'ticket_id' => $this->id(),
            'observer_id' => $observerId,
            'user_id' => App::$user->isLogged() ? App::$user->id() : $observerId,
        ];

        if ($observer instanceof DataObject) {
            $functionArguments = self::$_db->getFunctionArgs('add_observer', 'ticket_api');

            foreach ($functionArguments as $arg => $argParams) {
                $arg = trim($arg, '_');
                if (!array_key_exists($arg, $params) && isset($observer[$arg])) {
                    $params[$arg] = $observer[$arg];
                }
            }
        }

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

        if (self::getObserverColumnName() == 'person_id') {
            if (!$observer instanceof DataObject) {
                $observer = static::listPersonById($observerId);
            }
        } else {
            if (!$observer instanceof DataObject) {
                $observer = User::bufferedInstance($observerId);
            }
        }

        if (!is_array($this->_observers)) {
            $this->getObservers();
        }

        $this->_observers[$observerId] = $observer;
        $this->_observerIds[] = $observerId;

        return $result;
    }

    /**
     * Removes person from ticket observers
     *
     * @param PersonInterface|int $observer
     * @return int
     * @throws PersonIsNotObserverException
     * @throws Exception
     */
    public function removeObserver($observer)
    {
        if (!$this->isObserver($observer)) {
            throw new PersonIsNotObserverException();
        }

        $observerId = $observer instanceof DataObject ? $observer->id() : $observer;

        // we allow to remove observer when user is not logged when he accepts ticket by email link
        $params = [
            'ticket_id' => $this->id(),
            'observer_id' => $observerId,
            'user_id' => App::$user->isLogged() ? App::$user->id() : $observerId,
        ];

        unset($this->_observers[$observerId]);
        if (($key = array_search($observerId, $this->_observerIds)) !== false) {
            unset($this->_observerIds[$key]);
        }

        return self::$_db->execFunction('ticket_api.remove_observer', $params);
    }


    /**
     * Returns true if user is ticket observer
     *
     * @param PersonInterface|int $person
     * @return bool
     * @throws Exception
     */
    public function isObserver($person)
    {
        $observerId = $person instanceof DataObject ? $person->id() : $person;

        return in_array($observerId, $this->getObserverIds());
    }


    /**
     * Returns observer column name (person_id/user_id) in current database implementation
     * @return string
     */
    public static function getObserverColumnName()
    {
        $columns = App::$di['db']->getColumns('app.ticket_observer_tab');

        if (array_key_exists('person_id', $columns)) {
            return 'person_id';
        }

        return 'user_id';
    }


    /**
     * Reloads ticket observers
     *
     * @param bool $allowDuplicatedQuery
     * @return PersonInterface[]
     * @throws Exception
     */
    public function reloadObservers($allowDuplicatedQuery = false)
    {
        $this->_observers = null;

        if ($allowDuplicatedQuery) {
            self::$_db->checkDuplicatedQueries(false);
        }

        $this->getObservers();

        if ($allowDuplicatedQuery) {
            self::$_db->checkDuplicatedQueries(true);
        }

        return $this->_observers;
    }
}
