<?php

namespace Velis\Bpm\Ticket;

use Exception;
use InvalidArgumentException;
use LogicException;
use ReflectionException;
use RuntimeException;
use User\User;
use Velis\App;
use Velis\Arrays;
use Velis\App\Setting;
use Velis\Bpm\Person;
use Velis\Dictionary;
use Velis\Exception\BusinessLogicException;
use Velis\Lang;
use Velis\Model\DataObject;
use Velis\Model\RelationLoader\Relations\HasOneRelation;
use Velis\Model\Sanitizable;
use Velis\Notification\Recipient;
use Velis\ParameterBag;
use Velis\User\UserProvider;

/**
 * Changelog entry model
 * @author Olek Procki <olo@velis.pl>
 */
class Log extends DataObject implements Sanitizable
{
    /**
     * Core actions
     */
    public const CREATE   = 'Create';
    public const POST_ADD = 'AddPost';
    public const EDIT     = 'Edit';
    public const DELEGATE = 'Delegate';


    /**
     * @var \Velis\Notification
     */
    protected $_notification;


    /**
     * Actions hidden in history tab
     * @var array
     */
    protected static $_historyExcludedActions = ['Create'];


    /**
     * Related post
     * @var Post
     */
    protected $_post;


    /**
     * Related ticket
     * @var Ticket
     */
    protected $_ticket;

    /**
     * Labels attached to log
     * @var int[]
     */
    protected ?array $_labels = [];

    /**
     * Additional notification recipients
     * @var \Velis\User[]
     */
    protected $_recipients = [];


    /**
     * Last notification recipients
     * @var \Velis\User[]
     */
    protected $_lastRecipients = [];


    /**
     * Notification visibility mode
     * @var string
     */
    protected $_notificationVisibility = Post::TYPE_PUBLIC;


    /**
     * Exclude post author from notification
     * @var bool
     */
    protected $_excludeAuthor = false;


    /**
     * Returns related SQL table name
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.ticket_log_tab';
    }

    /**
     * Returns view for listing
     * @return string
     */
    protected function _getListDatasource()
    {
        return 'app.ticket_history th';
    }


    /**
     * Log constructor
     *
     * @param mixed $data
     * @param Post  $post
     */
    public function __construct($data = null, Post $post = null)
    {
        parent::__construct($data);
        if ($post) {
            $this->setPost($post);
        }
    }


    /**
     * Creates new empty instance for ticket
     *
     * @param Ticket|int $ticket
     * @param Post       $post
     *
     * @return Log
     *
     * @throws InvalidArgumentException
     */
    public static function factory($ticket, Post $post = null)
    {
        if (!($ticket instanceof Ticket)) {
            $ticket = Ticket::instance($ticket);
        }

        $instance = new static(
            [
                'ticket_id'                => $ticket->id(),
                'ticket_status_id'         => $ticket->getStatusId(),
                'direction'                => $ticket->direction,
            ],
            $post
        );

        if (self::hasField('user_id')) {
            $instance['user_id'] = $post['user_id'] ?? App::$user->id();
        } elseif (self::hasField('person_id')) {
            $instance['person_id'] = $post['person_id'] ?: App::$user->id();
        }

        $instance->_ticket = $ticket;

        return $instance;
    }


    /**
     * Sets log related post
     *
     * @param Post $post
     * @throws InvalidArgumentException
     */
    public function setPost(Post $post)
    {
        if (isset($this['ticket_post_id']) && $post->id() != $this['ticket_post_id']) {
            throw new InvalidArgumentException('Log entry already matched to another post');
        }

        $this->_post = $post;

        if (!isset($this['ticket_post_id'])) {
            $this['ticket_post_id'] = $post->id();
        }

        $this->_notificationVisibility = $post->ticket_post_visibility_id;
    }


    /**
     * Sets notification visibility
     * @param string $visibility
     */
    public function setNotificationVisibility($visibility)
    {
        $this->_notificationVisibility = $visibility;
    }


    /**
     * Returns log actions
     * @return array
     */
    public static function getActions()
    {
        return Dictionary::get('app.ticket_log_action_tab');
    }


    /**
     * Returns change type ID
     * @return string
     */
    public function getActionId()
    {
        return $this->ticket_log_action_id;
    }


    /**
     * Returns change type name
     * @return string
     */
    public function getActionName()
    {
        $actions = self::getActions();
        return $actions[$this->getActionId()];
    }


    /**
     * Returns action name
     *
     * @param string $actionId
     * @return string
     */
    public static function getActionNameById($actionId)
    {
        $actions = self::getActions();
        return (string)$actions[$actionId];
    }


    /**
     * Returns related ticket post
     * @return Post
     */
    public function getPost()
    {
        $postClass = App::getService('ticketService')->getPostClass();
        if (!isset($this->_post) && $this->ticket_post_id) {
            $this->_post = $postClass::instance($this->ticket_post_id);
        }
        return $this->_post;
    }


    /**
     * Returns ticket
     * @return Ticket
     */
    public function getTicket()
    {
        if (!isset($this->_ticket)) {
            $this->_ticket = Ticket::instance($this->ticket_id);
        }
        return $this->_ticket;
    }


    /**
     * Sets ticket instance
     *
     * @param Ticket $ticket
     * @return Log
     * @throws BusinessLogicException
     */
    public function setTicket(Ticket $ticket)
    {
        if ($this['ticket_id'] && $this['ticket_id'] != $ticket->id()) {
            throw new BusinessLogicException(Lang::get('TICKET_INVALID_NO'));
        }
        $this->_ticket = $ticket;

        if (!$this['ticket_id']) {
            $this['ticket_id'] = $ticket->id();
        }

        return $this;
    }

    /**
     * Returns change author
     */
    public function getUser(): \Velis\User
    {
        /** @var UserProvider $userProvider */
        $userProvider = App::getService('userProvider');

        return $userProvider->getUser($this->user_id);
    }


    /**
     * Returns anonymized data for $key
     *
     * @param string $key
     * @return string
     */
    public function offsetGet($key)
    {
        // replace personal data in log description
        if (
            $key == 'description' &&
            App::$config->anonymize->enabled &&
            App::$config->anonymize->logPatterns
        ) {
            // convert config node to array
            $anonymizePatterns = Arrays::toArray(App::$config->anonymize->logPatterns);

            return preg_replace(
                array_keys($anonymizePatterns),
                array_values($anonymizePatterns),
                parent::offsetGet($key)
            );
        }

        return parent::offsetGet($key);
    }


    /**
     * Returns full history for ticket including posts
     *
     * @param Ticket|int $ticket
     * @param bool $loadFiles
     * @param array $params
     * @return Log[]
     * @throws DataObject\NoColumnsException
     * @throws ReflectionException
     */
    public static function getTicketHistory($ticket, $loadFiles = false, $params = array())
    {
        if (!($ticket instanceof Ticket)) {
            $ticket = new Ticket($ticket);
        }
        $params['ticket_id']     = $ticket->id();
        $params['hide_excluded'] = true;

        $history = [];
        $posts   = [];

        if (self::hasField('is_sticky', true)) {
            $order = 'is_sticky DESC, date_modified DESC';
        } else {
            $order = 'date_modified DESC, ticket_log_id DESC';
        }

        if (App::$user->isLogged()) {
            if (!App::$user->hasPriv('Ticket', 'PrivatePostsAccess')) {
                $field = self::hasField('person_id') ? 'person_id' : 'user_id';
                self::$_listConditions[] = "(ticket_post_visibility_id != 'Private' OR $field = " . App::$user['user_id'] . ")";
            }
        }

        foreach (self::listAll($params, $order) as $row) {
            $entry = new self($row);

            if ($entry['ticket_post_id']) {
                $postClass = App::getService('ticketService')->getPostClass();
                $entry->_post = new $postClass(
                    Arrays::extractFields($entry, [
                        'ticket_post_id',
                        'ticket_id',
                        'ticket_post_visibility_id',
                        'content',
                        'user_id',
                        'person_id',
                    ])
                );
                $entry->_post['date_entered'] = $entry['date_modified'];
                $posts[] = $entry->_post;
            }
            $history[$entry->id()] = $entry;
        }
        if ($loadFiles) {
            $ticket->setFiles(Post::loadPostsFiles($posts));
        }

        return $history;
    }


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

        if (static::$_historyExcludedActions && $params['hide_excluded']) {
            self::$_listConditions[] = "(
                NOT ticket_log_action_id_array && ARRAY['" . implode("','", static::$_historyExcludedActions) . "']::varchar[]
                OR (ticket_post_id IS NOT NULL AND content IS NOT NULL)
            )";
        }

        if ($params['excluded_actions']) {
            self::$_listConditions[] = "(NOT ticket_log_action_id_array && ARRAY['" . implode("','", array_filter($params['excluded_actions'])) . "']::varchar[])";
        }

        if ($params['ticket_log_action_id'] && is_string($params['ticket_log_action_id'])) {
            self::$_listConditions[] = ":ticket_log_action_id = ANY (regexp_split_to_array(ticket_log_action_id, E','))";
            self::$_listParams['ticket_log_action_id'] = $params['ticket_log_action_id'];
            unset($params['ticket_log_action_id']);
        }

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


    /**
     * Adds additional notification recipient
     *
     * @param \Velis\User|int $recipient
     * @return Log
     */
    public function addRecipient($recipient)
    {
        if (!$recipient instanceof Recipient) {
            throw new LogicException('Recipient must implement \Velis\Notification\Recipient interface!');
        }
        if ($recipient) {
            $this->_recipients[] = $recipient;
        }
        return $this;
    }


    /**
     * Adds additional recipient
     *
     * @param \Velis\User[]|int[] $recipients
     * @return Log
     */
    public function addRecipients(array $recipients)
    {
        foreach ($recipients as $recipient) {
            $this->addRecipient($recipient);
        }
        return $this;
    }


    /**
     * Adds recipient(s) of user type (object(s)/id(s)) can be provided
     *
     * @param \Velis\User|int[]|int
     * @return Log
     */
    public function addRecipientUser($user)
    {
        if ($user instanceof \Velis\User) {
            $this->addRecipient($user);
        } else {
            if (is_array($user) && count($user)) {
                $this->addRecipients(User::get($user));
            } else {
                $this->addRecipient(User::get($user));
            }
        }

        return $this;
    }


    /**
     * Returns post notification recipients
     *
     * @param string $type class name
     * @return Recipient[]
     */
    public function getRecipients($type = null)
    {
        static $postSubscription = [];
        static $recipientsPerVisibility = [];

        if ($this->_notificationVisibility == Post::TYPE_PRIVATE) {
            if (!Setting::get('PrivatePostsNotifications')) {
                return [];
            }
            $recipientsPerVisibility = $this->getTicket()->getPrivatePostRecipients();
        } elseif ($this->_notificationVisibility == Post::TYPE_PROTECTED) {
            $recipientsPerVisibility = $this->getTicket()->getProtectedPostRecipients();
        } else {
            $recipientsPerVisibility = $this->getTicket()->getNotificationRecipients();
        }

        $recipients = [];

        foreach (array_filter($recipientsPerVisibility) as $recipient) {
            if ($recipient instanceof \Velis\User) {
                // skip protected/private posts for customers
                // skip users who switch of notifications
                if (($recipient->hasCompany() && $this->_notificationVisibility != Post::TYPE_PUBLIC)) {
                    continue;
                } elseif (array_key_exists($recipient['user_id'], $postSubscription)) {
                    if (!$postSubscription[$recipient['user_id']]) {
                        continue;
                    }
                } else {
                    if ($this->_notificationVisibility == Post::TYPE_PRIVATE) {
                        $postSubscription[$recipient['user_id']] = $recipient->settings('NotifyNewPost') && $recipient->settings('PrivatePostsNotifications');
                    } else {
                        $postSubscription[$recipient['user_id']] = $recipient->settings('NotifyNewPost');
                    }
                    if (!$postSubscription[$recipient['user_id']]) {
                        continue;
                    }
                }
            }
            if ($recipient instanceof Person && $this->_notificationVisibility != Post::TYPE_PUBLIC) {
                continue;
            }

            if ($this->_excludeAuthor && array_intersect([$this['user_id'], $this['person_id']], [$recipient['user_id'], $recipient['person_id']])) {
                continue;
            }
            $recipients[] = $recipient;
        }
        $recipients = array_merge($this->_recipients, $recipients);

        if ($type != null) {
            foreach ($recipients as $key => $recipient) {
                if (!$recipient instanceof $type) {
                    unset($recipients[$key]);
                }
            }
        }
        return $recipients;
    }


    /**
     * Returns last post notification recipients
     *
     * @return Recipient[]
     */
    public function getLastRecipients()
    {
        if (!$this->_lastRecipients) {
            $this->_lastRecipients = $this->getRecipients();
        }
        return $this->_lastRecipients;
    }


    /**
     * Prepares notification object
     * @return Notification
     */
    protected function _prepareNotification()
    {
        if (!isset($this->_notification)) {
            if ($this->ticket_log_action_id === Log::DELEGATE) {
                $notificationType = Notification::TICKET_DELEGATED;
            } else {
                $notificationType = $this->ticket_log_action_id == Log::CREATE ? Notification::TICKET_CREATED : Notification::TICKET_CHANGE;
            }

            $this->_notification = new \Ticket\Notification(
                $this->getTicket(),
                $notificationType
            );

            $connectedLogs = array();
            $changes       = array();

            if ($this->getPost()) {
                $this->_notification->post = $this->getPost();

                if ($this->_post->id()) {
                    // look for other logs connected with this post
                    $params = $this->_post->_getPrimaryKeyParam();
                    $params['ticket_id'] = $this->ticket_id;

                    foreach (self::listAll($params) as $log) {
                        $connectedLogs[] = $log;
                    }
                }
            } else {
                $connectedLogs[] = $this;
            }

            foreach ($connectedLogs as $log) {
                if ($log->ticket_log_action_id != self::POST_ADD) {
                    $changeDescription = $log->description ?: $log->getTranslatedName();
                    $changes[] = trim($changeDescription);
                }
            }
            $this->_notification->changes = trim(implode("\n", $changes));
            $this->_notification->log = $this;

            if ($this->_notification instanceof LogContainerInterface) {
                $this->_notification->setTicketLog($this);
            }
        }
        return $this->_notification;
    }


    /**
     * Returns prepared notification
     * @return \Velis\Notification
     */
    public function getNotification()
    {
        return $this->_prepareNotification();
    }


    /**
     * Returns post files (if any attached)
     * @return array
     */
    protected function _getFiles()
    {
        $files = array();
        $post = $this->getPost();

        if ($post) {
            if ($post->getFiles()) {
                foreach ($post->getFiles() as $file) {
                    $file['date_entered'] = $post['date_entered'];

                    $files[] = array(
                        'filename' => $file->filename,
                        'link'     => $file->getStoragePath(),
                        'url'      => $file->getDownloadUrl(),
                        'hash'     => $file['hash']
                    );
                }
            }
        }
        return $files;
    }


    /**
     * Sends change notification
     *
     * @return Log
     * @throws RuntimeException
     */
    public function notify()
    {
        $notification = $this->_prepareNotification();
        $recipients   = $this->getRecipients();

        $post = $this->getPost();

        if (count($recipients) || $_SERVER['ON_DEV']) {
            try {
                $notification->send($recipients, $this->_getFiles());
                if ($post) {
                    $post->saveRecipients($recipients);
                }
            } catch (Exception $e) {
                throw new RuntimeException('Wystąpił błąd podczas wysyłania powiadomienia email: ' . $e->getMessage(), 0, $e);
            }
        }
        $this->_lastRecipients = $recipients;
        $this->_execTriggers('notify');

        return $this;
    }


    /**
     * Inserts new row
     *
     * @param bool $updateObjectId
     * @return DataObject|void
     * @throws Exception
     */
    public function add($updateObjectId = false)
    {
        parent::add($updateObjectId);

        if ($this->id()) {
            $stm = self::$_db->prepare("INSERT INTO app.ticket_log_label_tab (ticket_log_id, label_id, action) VALUES (:log_id, :label_id, :action);");

            foreach ($this->_labels as $id => $action) {
                $stm->execute(['log_id' => $this->id(), 'label_id' => $id, 'action' => $action]);
            }
        }
    }


    /**
     * Check if log has attached some labels before adding
     * @return int
     */
    public function hasLabels()
    {
        return count($this->_labels);
    }


    /**
     * Removes label
     * @param int $label
     */
    public function addLabel($label)
    {
        $this->_labels[(int) $label] = '+';
    }


    /**
     * Adds label to log
     * @param int $label
     */
    public function removeLabel($label)
    {
        $this->_labels[(int) $label] = '-';
    }


    /**
     * Generates logs for added/removed labels
     * @return string[]
     */
    public function labelLog()
    {
        $userLabels = App::$user->getLabels();

        if ($this->_post) {
            $ticket = $this->_post->getTicket();
            if ($this->_post->ticket_post_visibility_id == Post::TYPE_PUBLIC && Ticket::hasField('company_visible') && $ticket->company_visible) {
                return [];
            }
        }

        $log = [];
        foreach ($this->_labels as $key => $label) {
            if (!isset($userLabels[$key])) {
                continue;
            }
            $log[] = ($label == '+' ? "{GENERAL_LABEL_ADDED} " : "{GENERAL_LABEL_REMOVED} ") . "\"" . $userLabels[$key]->getName() . "\"";
        }

        return $log;
    }


    /**
     * Exclude post author from notification
     */
    public function excludeAuthor()
    {
        $this->_excludeAuthor = true;
    }


    /**
     * Add excluded log action
     * @param string $action
     * @return array
     */
    public static function addExcludedAction($action)
    {
        array_push(self::$_historyExcludedActions, $action);
        return self::$_historyExcludedActions;
    }

    /**
     * @return HasOneRelation<User>
     */
    public function relationUser(): HasOneRelation
    {
        return $this->hasOne(User::class, 'user_id');
    }
}
