<?php

namespace Velis\Bpm\Email;

use ArrayIterator;
use Company\Company;
use Exception;
use Psr\SimpleCache\InvalidArgumentException;
use User\User;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Person;
use Velis\Bpm\Ticket\Log;
use Velis\Bpm\Ticket\Notification as TicketNotification;
use Velis\Bpm\Ticket\Post;
use Velis\Bpm\Ticket\Ticket;
use Velis\Bpm\Workflow;
use Velis\Bpm\Workflow\Event;
use Velis\Bpm\Workflow\Subject;
use Velis\Db\Exception as DbException;
use Velis\Db\Lob;
use Velis\Exception as VelisException;
use Velis\Filter;
use Velis\Lang;
use Velis\Mail\Parser;
use Velis\Model\DataObject;
use Velis\Model\DataObject\NoColumnsException;
use Velis\Model\Sanitizable;
use Velis\Notification\HeadersTrait;
use Velis\Output;
use Zend\Mail\Header\ContentType;
use Zend\Mail\Header\HeaderInterface;
use Zend\Mail\Headers;
use Zend\Mail\Storage\Message\MessageInterface;
use Zend\Mail\Storage\Part;

/**
 * Imported email message model
 * @author Olek Procki <olo@velis.pl>
 */
class Message extends DataObject implements Sanitizable, Subject
{
    use HeadersTrait;


    public const EVENT_RECEIVED = 'EmailReceived';


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


    /**
     * Default list order
     * @var string
     */
    protected static $_listDefaultOrder = 'date_sent DESC';


    /**
     * @var Attachment[]|null
     */
    protected $_attachments;


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


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


    /**
     * Sender person
     * @var Person
     */
    protected $_person;


    /**
     * Email sender
     * @var User|Person
     */
    protected $_owner;


    /**
     * Sender company
     * @var Company
     */
    protected $_company;


    /**
     * Source message data
     * @var \Zend\Mail\Storage\Message
     */
    protected $_sourceMessage;


    /**
     * Parser instance
     * @var Parser
     */
    protected $_parser;


    /**
     * @var Ticket[]
     */
    protected static $_tickets;

    /**
     * @param mixed $data
     * @param Account $account
     * @throws NoColumnsException
     */
    public function __construct($data = null, $account = null)
    {
        if ($data instanceof MessageInterface) {
            $this->_sourceMessage = $data;
            $this->_parser = new Parser();

            if ($account) {
                $this['email_account_id'] = $account instanceof Account ? $account->id() : $account;
            }

            $this
                ->_resolveHeaders()
                ->_resolveContents()
            ;
        } else {
            if (isset($data['additional_headers'])) {
                $this->setAdditionalHeaders($data['additional_headers']);
            }
            parent::__construct($data);
        }
    }

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


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


    /**
     * Returns related email account
     * @return Account
     */
    public function getAccount()
    {
        return Account::get($this->email_account_id);
    }


    /**
     * Resolves specific attributes (overload for some extra params handling)
     * @return Message
     */
    public function resolveAttributes()
    {
        return $this;
    }


    /**
     * Resolves specific parse conditions (overload for some extra params handling)
     * @return bool
     */
    public function customParse()
    {
        return false;
    }


    /**
     * Returns true for automatic messages
     * @return bool
     */
    public function isAutoSubmitted()
    {
        return $this->auto_submitted == 1;
    }

    /**
     * Checks access to email message
     * @return bool
     */
    public function checkAccess()
    {
        if (class_exists('\Email\Account')) {
            $availableAccounts = \Email\Account::getUserAvailableAccounts();
        } else {
            $availableAccounts = Account::getUserAvailableAccounts();
        }

        // access allowed for available accounts
        if ($this->getAccount()->belongs($availableAccounts)) {
            return true;
        }

        // can't access pending messages from other accounts
        if ($this->is_pending) {
            return false;
        }

        $ticket = $this->getTicket();

        if ($ticket->checkAccess()) {
            if (!App::$user->hasCompany()) {
                // internal users with ticket access are able to see any post in the ticket
                return true;
            }

            if (!$post = $this->getPost()) {
                // email is a ticket description
                return true;
            } else {
                // email is a ticket post (only public posts are visible for external users)
                return $post->isPublic();
            }
        }

        return false;
    }

    /**
     * Resolves message headers
     * @return Message
     * @throws NoColumnsException
     */
    protected function _resolveHeaders()
    {
        if ($this->_sourceMessage) {
            $headers = $this->_sourceMessage->getHeaders();

            if ($this->_hasField('additional_headers')) {
                foreach (self::$_acceptableAdditionalHeaders as $header) {
                    if ($headers->has($header)) {
                        $this->setAdditionalHeader(
                            $header,
                            $headers->get($header)->getFieldValue()
                        );
                    }
                }
                if ($this->hasAdditionalHeaders()) {
                    $this['additional_headers'] = Output::jsonEncode(
                        $this->getAdditionalHeaders()
                    );
                }
            }

            if ($headers->has('subject')) {
                $this['subject'] = $this->_sourceMessage->subject;
                $additionalHeaders = $this->getAdditionalHeaders();

                // Resolve ticket_id from subject only when message
                // was not sent by other Singu instance
                if (
                    !$additionalHeaders ||
                    $additionalHeaders['X-Instance-Acro'] == App::$config->settings->instanceAcro
                ) {
                    $this->_resolveTicketFromSubject();
                    // Use external ticket id when message was sent from another Singu instance
                } elseif (
                    $additionalHeaders['X-Instance-Acro'] &&
                    $additionalHeaders['X-Instance-Acro'] != App::$config->settings->instanceAcro
                ) {
                    if ($additionalHeaders['X-External-Ticket-Id']) {
                        // Try to find local ticket by External-Ticket-Id header from received message
                        $ticket = Ticket::instance($additionalHeaders['X-External-Ticket-Id']);
                        if ($ticket) {
                            // When ticket exists, connect it with this message
                            $this['ticket_id'] = $ticket->id();

                            // Assign external ticket number to local ticket
                            if (Ticket::hasField('email_integration_ticket_id') && !$ticket['email_integration_ticket_id']) {
                                // create empty ticket object to avoid further data overwriting
                                $ticketObject = new Ticket($ticket->id());
                                $ticketObject['email_integration_ticket_id'] = $additionalHeaders['X-Ticket-Id'];
                                $this->on('add', function () use ($ticketObject) {
                                    $ticketObject->modify();
                                });

                                // Mark message processed when it is a ticket created notification
                                if ($additionalHeaders['X-Ticket-Action'] == TicketNotification::TICKET_CREATED) {
                                    $this['auto_submitted'] = 1;
                                    $this['is_pending']     = 0;
                                }
                            }
                        }
                    }
                }
            } else {
                $this['subject'] = '[' . Lang::get('GENERAL_NO_SUBJECT') . ']';
            }

            $this['recipients_to']  = $this->_parser->getRecipients($this->_sourceMessage, 'to');
            $this['recipients_cc']  = $this->_parser->getRecipients($this->_sourceMessage, 'cc');
            $this['recipients_bcc'] = $this->_parser->getRecipients($this->_sourceMessage, 'bcc');

            if ($headers->has('date')) {
                $timeSent = strtotime($this->_sourceMessage->date);
                if (date('Y', $timeSent) != '0000') {
                    $this['date_sent'] = date('Y-m-d H:i:s', $timeSent);
                } else {
                    $this['date_sent'] = date('Y-m-d H:i:s');
                }
            } else {
                $this['date_sent'] = date('Y-m-d H:i:s');
            }

            $sender = $this->_parser->getSender($this->_sourceMessage);
            $regexp = '/\s*(([^\<]*?) <)?<?([^>]+)>?\s*/i';

            if (preg_match($regexp, $sender, $matches)) {
                $this['sender_name']  = $matches[2] ?: $matches[3];
                $this['sender_email'] = $matches[3];
            } else {
                $this['sender_name']  = $sender;
                $this['sender_email'] = $sender;
            }

            $generateMessageIdFrom = [
                $this['date_sent'],
                $this['sender_email'],
                $this['subject'],
            ];
            $this['message_id'] = $this->_parser->getMessageId($this->_sourceMessage, true, $generateMessageIdFrom);

            if ($headers->has('X-Instance-Acro')) {
                $this['instance_acro'] = $this->_sourceMessage->XInstanceAcro;
            }

            $this['raw_headers'] = Output::jsonEncode($headers->toArray());
        }

        return $this;
    }


    /**
     * Resolves body & attachments contents
     *
     * @param bool $decodeQuotedPrintable
     * @return $this
     */
    protected function _resolveContents($decodeQuotedPrintable = true)
    {
        if ($this->_sourceMessage) {
            // find attached files
            $this->_attachments = [];

            $parsedMessage = $this->_parser->parse($this->_sourceMessage);

            foreach ($this->_parser->getAttachedEmails() as $emailAttached) {
                $this->_attachments[] = new Attachment(
                    [
                        'filename' => $emailAttached['filename'],
                        'type' => $emailAttached['type'],
                    ],
                    $emailAttached['content']
                );
            }

            if ($decodeQuotedPrintable) {
                $this['body'] = quoted_printable_decode($parsedMessage['contentTextPlain']);
            } else {
                $this['body'] = $parsedMessage['contentTextPlain'];
            }

            // if txt part empty - use HTML
            if (!strlen($this['body']) && strlen($parsedMessage['contentTextHtml'])) {
                $this['body'] = str_replace(['<br>', '<br />', '<p>', '</p>'], "\n", $parsedMessage['contentTextHtml']);
                $this['body'] = str_replace('&nbsp;', ' ', $this['body']);
            }

            $this['body_html'] = new Lob(
                $this->_parser->stripMetaTags($parsedMessage['contentTextHtml'])
            );
            $this['body'] = strip_tags(
                Output::convertEncoding($this['body'])
            );

            $parsedMessage['contentTextCalendar'] = $parsedMessage['contentTextCalendar'] ?? null;
            if ($parsedMessage['contentTextCalendar']) {
                $this['body_calendar'] = $parsedMessage['contentTextCalendar'];
            }

            foreach ($this->_sourceMessage as $part) {
                $this->_extractAttachments($part);
            }

            foreach ($this as $key => $value) {
                if (!mb_check_encoding($value) && $key != 'body_html') {
                    $this[$key] = mb_convert_encoding($value, 'utf-8');
                }
            }
        }

        return $this;
    }


    /**
     * Resolves ticket from subject id
     * @return Message
     */
    protected function _resolveTicketFromSubject()
    {
        if (preg_match('/(argument #[1-9](?![0-9+])|parameter #[1-9](?![0-9+]))/i', $this['subject'])) {
            return $this;
        }

        preg_match('/#([0-9]+)/', $this['subject'], $matches);

        if (count($matches)) {
            if ($matches[1] < 2147483640) {
                $this->_ticket = self::$_tickets[$matches[1]] ?? null;
                if ($this->_ticket) {
                    $this['ticket_id'] = $this->_ticket->id();
                } elseif ($this->_ticket = Ticket::instance($matches[1])) {
                    self::$_tickets[$this->_ticket->id()] = $this->_ticket;
                    $this['ticket_id'] = $this->_ticket->id();
                }
            }
        }

        return $this;
    }


    /**
     * Extract attachments recursive
     *
     * @param Part $part
     * @param int $attNo
     */
    protected function _extractAttachments($part, &$attNo = null)
    {
        if (!$attNo) {
            $attNo = 1;
        }

        $headers = $part->getHeaders();

        if ($headers && $headers->has('content-disposition')) {
            $contentDisposition = $headers->get('content-disposition');
        }

        $contentType = null;
        $filename = null;

        if ($headers && $headers->has('content-type')) {
            try {
                $contentType = $headers->get('content-type');
            } catch (Exception $e) {
                $contentType = new ContentType();
                $contentType->setType('application/octet-stream');
            }
        }

        if ($contentType && $contentType->getParameter('name')) {
            $filename = $contentType->getParameter('name');
        } elseif (isset($contentDisposition) && stripos($contentDisposition->getFieldValue(), 'filename') !== false) {
            $value  = str_replace(Headers::FOLDING, " ", $contentDisposition->getFieldValue());
            $values = preg_split('#\s*;\s*#', $value);

            if (count($values)) {
                foreach ($values as $keyValuePair) {
                    if (strpos($keyValuePair, '=')) {
                        [$key, $value] = explode('=', $keyValuePair, 2);
                        $value = trim($value, "'\" \t\n\r\0\x0B");
                        if ($key == 'filename') {
                            $filename = $value;
                        }
                    }
                }
            }
        }

        if ($filename || ($contentType && stripos($contentType->getFieldValue(), 'image') !== false)) {
            if ($contentType && !$filename) {
                $filename  = Lang::get('EMAIL_ATTACHMENT') . $attNo++;
                $filename .= '.' . strtolower(str_replace('image/', '', $contentType->getFieldValue()));
            }
            if (!$headers->has('content-transfer-encoding') || strtolower($headers->get('content-transfer-encoding')->getFieldValue()) == 'base64') {
                $contents = base64_decode($part->getContent());
            } elseif ($headers->has('content-transfer-encoding') && $headers->get('content-transfer-encoding')->getFieldValue() == 'quoted-printable') {
                $contents = quoted_printable_decode($part->getContent());
            } else {
                $contents = $part->getContent();
            }

            if (empty($contents)) {
                return;
            }

            $contentIdValue = '';
            if ($headers->has('content-id')) {
                $contentId = $headers->get('content-id');

                // multiple content-id header occurred in this message
                if ($contentId instanceof ArrayIterator) {
                    foreach ($contentId as $singleHeader) {
                        if ($singleHeader instanceof HeaderInterface) {
                            if ($singleHeader->getFieldValue()) {
                                $contentIdValue = $singleHeader->getFieldValue();
                                break;
                            }
                        }
                    }
                } elseif ($contentId instanceof HeaderInterface) {
                    $contentIdValue = $contentId->getFieldValue();
                }
            }

            $this->_attachments[] = new Attachment(
                [
                    'filename' => $filename,
                    'type' => $contentType ? $contentType->getType() : null,
                    'cid' => $contentIdValue,
                ],
                $contents
            );
        } elseif ($contentType && strpos($contentType->getFieldValue(), 'multipart') !== false) {
            try {
                $part->getChildren();
            } catch (Exception $e) {
                $parser = new Parser();
                $parser->handleMimeEndMissing($part);
            }

            foreach ($part as $p) {
                $this->_extractAttachments($p, $attNo);
            }
        }
    }


    /**
     * Adds object and saves attachments
     *
     * @param bool $updateObjectId
     * @return Message
     * @throws Exception
     */
    public function add($updateObjectId = true)
    {
        // keep visibility info
        $postVisibility = null;
        if (isset($this['ticket_post_visibility_id'])) {
            $postVisibility = $this['ticket_post_visibility_id'];
        }

        if (!isset($this['sender_name']) || trim($this['sender_name']) === '') {
            $this['sender_name'] = '[ERROR] No sender name <' . $this['sender_email'] . '>';
        }

        try {
            parent::add($updateObjectId);
        } catch (DbException $e) {
            // in case of database encoding problem
            // try to retrieve message contents without quoted-printable decoding
            if ($e->getCode() == 22021 && $this->_sourceMessage) {
                $this->_resolveContents(false);
                parent::add($updateObjectId);
            } else {
                throw $e;
            }
        }

        if ($this->_attachments) {
            foreach ($this->_attachments as $key => $attachment) {
                $attachment['email_message_id'] = $this->id();

                if (strlen($attachment['filename'])) {
                    $attachment->add();
                }

                unset($this->_attachments[$key]);
                $this->_attachments[$attachment->id()] = $attachment;
            }
        }
        $this['ticket_post_visibility_id'] = $postVisibility;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function modify($checkDiff = false)
    {
        if ($this->offsetExists('body_html')) {
            if (!$this['body_html'] instanceof Lob) {
                unset($this['body_html']);
            }
        }

        return parent::modify($checkDiff);
    }


    /**
     * Returns attached files
     * @return Attachment[]
     */
    public function getAttachments()
    {
        if (!isset($this->_attachments)) {
            $this->_attachments = Attachment::listAll(array(
                'email_message_id' => $this->id()
            ));
        }

        return $this->_attachments;
    }


    /**
     * Loads messages attachments at once
     * @param Message[] $items
     */
    public static function loadItemsAttachments($items)
    {
        if (!count($items)) {
            return;
        }

        $idList = self::getCollectionIds($items);

        foreach ($items as $message) {
            $message->_attachments = [];
        }

        foreach (Attachment::listAll(['email_message_id' => $idList]) as $attachment) {
            foreach ($items as $message) {
                if ($attachment->email_message_id == $message->id()) {
                    $message->_attachments[$attachment->id()] = $attachment;
                }
            }
        }
    }


    /**
     * Converts email message to ticket post
     *
     * @param Ticket|int $ticket
     * @param array $selectedAttachments
     * @param $direction
     * @return Message
     * @throws Exception
     */
    public function toPost($ticket, $selectedAttachments = array(), $direction = ">")
    {
        self::$_db->startTrans();

        try {
            if (!strlen(trim($this['body'])) && strlen(trim($this['body_html']))) {
                $content = strip_tags(str_replace(array('<br>', '<br />', '<p>', '</p>'), "\n", $this['body_html']));
                $content = str_replace('&nbsp;', ' ', $content);

                if (!mb_check_encoding($content)) {
                    $content = mb_convert_encoding($content, 'utf-8', array('iso-8859-2'));
                }
            } else {
                $content = $this['body'];
            }

            if (!is_array($selectedAttachments)) {
                $selectedAttachments = array_filter(array($selectedAttachments));
            }

            if ($ticket instanceof Ticket) {
                $this->_ticket = $ticket;
            }

            $user = isset($this['owner_user_id']) ? User::bufferedInstance($this['owner_user_id']) : null;
            // When answering to Bid ticket, we need to set ticket visibility to "protected".
            if ($ticket['ticket_type_id'] === 'Bid') {
                $ticketVisibility = Post::TYPE_PROTECTED;
            } else {
                $ticketVisibility = $this['ticket_post_visibility_id'] ?: Post::TYPE_PUBLIC;

                if (
                    $user
                    && !$user->hasPriv('Ticket', 'InternalPostsAccess')
                    && !$user->hasPriv('Ticket', 'PrivatePostsAdd')
                ) {
                    $ticketVisibility = Post::TYPE_PUBLIC;
                }
            }

            $this->_post = new Post([
                'ticket_id'                 => $ticket instanceof Ticket ? $ticket->id() : $ticket,
                'content'                   => $content,
                'user_id'                   => $this['owner_user_id'],
                'person_id'                 => $this['person_id'],
                'direction'                 => $direction ?: ">",
                'ticket_post_visibility_id' => $ticketVisibility,
                'partner_id'                => $user?->getPartnerId(),
            ]);


            $this->_post->add(true)->load();

            $log = Log::factory($ticket, $this->_post);

            $log['ticket_log_action_id'] = Log::POST_ADD;
            $log['direction']            = $direction ?: ">";
            $log['description']          = Output::cut($this->_post['content'], 125);
            $log['user_id']              = $this['owner_user_id'];
            $log['person_id']            = $this['person_id'];

            $this->_onPost($log, $user);

            $log->add();

            $ticket->setInvokedByUser(new User($this->getOwner()));

            $restore = App::$di['db']->checkDuplicatedQueries(false);
            Event::fire(Ticket::EVENT_CHANGED, $ticket, $ticket->getCurrentInstance());
            App::$di['db']->checkDuplicatedQueries($restore);

            foreach ($this->getAttachments() as $attachment) {
                if (
                    in_array($attachment->id(), $selectedAttachments)
                    && $attachment->exists()
                    && $attachment->getType() !== null
                ) {
                    try {
                        $file = $attachment->toPostFile($this->_post);

                        if ($attachment->cid) {
                            $html = (string)$this->body_html;

                            $this['body_html'] = new Lob(
                                str_ireplace(
                                    'cid:' . trim($attachment->cid, '<>'),
                                    'http://' . $_SERVER['SERVER_NAME'] . $file->getDownloadUrl(),
                                    $html
                                )
                            );
                        }
                    } catch (Exception $e) {
                        VelisException::raise('Failed to add post file from email message', $e->getCode(), $e);
                    }
                } else {
                    $attachment->remove();
                }
            }
            $this['is_pending']     = 0;
            $this['ticket_post_id'] = $this->_post->id();
            $this['ticket_id']      = $this->_post->ticket_id;

            if (!$this['body_html'] instanceof Lob) {
                unset($this['body_html']);
            }

            $this->modify();

            if ($ticket->isFinished()) {
                if (!$this->hasAdditionalHeader('X-External-Ticket-Id')) {
                    $ticket->reopen($this->_post);
                }
            }
        } catch (Exception $e) {
            self::$_db->rollback();
            throw $e;
        }

        self::$_db->commit();

        if (
            preg_match('/\[MT\s#\d+\]\s\[[a-z0-9\@\.]+\]\s#([0-9]+)/', $this['subject']) ||
            $this->hasAdditionalHeader('X-External-Ticket-Id')
        ) {
            // exclude post author for email integration with external system
            $log->excludeAuthor();
        }

        if ($ticket['ticket_type_id'] !== 'Bid') {
            $log->notify();
        }

        return $this;
    }


    /**
     * On post add handler
     *
     * @param $log
     * It's a legacy code, so I am not brave enough to change it
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint
     * @return $this
     */
    protected function _onPost($log, ?\Velis\User $user = null)
    {
        return $this;
    }


    /**
     * Removes messages with attachments
     * @return bool
     */
    public function remove()
    {
        if ($this->getAttachments()) {
            foreach ($this->getAttachments() as $attachment) {
                $attachment->remove();
            }
        }

        return $this->_remove();
    }


    /**
     * {@inheritDoc}
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        if ($params['search']) {
            self::$_listConditions[] = '(body ILIKE :search OR subject ILIKE :search)';
            self::$_listParams['search'] = '%' . $params['search'] . '%';
        }

        if ($params['group']) {
            $dateAddedColumn = 'max_date_added';
            unset($params['group']);
        } else {
            $dateAddedColumn = 'date_added';
        }

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

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

        if ($params['sender_name']) {
            self::$_listConditions[] = 'sender_name ILIKE :sender';
            self::$_listParams['sender'] = '%' . $params['sender_name'] . '%';
            unset($params['sender_name']);
        }

        if ($params['timetocheck']) {
            $dateToCheck = date('Y-m-d H:i:s', strtotime("-{$params['timetocheck']} seconds"));
            self::$_listConditions[] = "$dateAddedColumn < :timetocheck";
            self::$_listParams['timetocheck'] = $dateToCheck;
        }

        self::$_listConditions[] = 'is_spam=:is_spam';
        self::$_listParams['is_spam'] = (int)($params['is_spam'] ? $params['is_spam'] : 0);

        self::$_listConditions[] = 'is_pending=:is_pending';
        self::$_listParams['is_pending'] = (int)(strlen($params['is_pending']) ? $params['is_pending'] : 1);

        if (self::hasField('auto_submitted')) {
            self::$_listConditions[] = 'auto_submitted=:auto_submitted';
            self::$_listParams['auto_submitted'] = (int)($params['auto_submitted'] ? $params['auto_submitted'] : 0);

            unset($params['auto_submitted']);
        }

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


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


    /**
     * Return related post
     * @return Post
     */
    public function getPost()
    {
        if (!isset($this->_post) && $this->ticket_post_id) {
            $this->_post = Post::instance($this->ticket_post_id);
        }
        return $this->_post;
    }


    /**
     * Returns sender person
     * @return Person
     */
    public function getPerson()
    {
        if (!isset($this->_person) && $this->person_id) {
            $this->_person = Person::instance($this->person_id);
        }
        return $this->_person;
    }


    /**
     * Returns email sender object
     * @return User|Person|null
     * @throws InvalidArgumentException
     */
    public function getOwner()
    {
        if (!isset($this->_owner)) {
            if ($this->_hasField('owner_user_id') && $this->owner_user_id) {
                $this->_owner = User::get($this->owner_user_id);
            } elseif ($this['person_id']) {
                $this->_owner = User::get($this['person_id']);

                if (!$this->_owner) {
                    $this->_owner = Person::instance($this['person_id']);
                }
            } else {
                $this->_owner = false;
            }
        }

        if ($this->_owner) {
            return $this->_owner;
        }

        return null;
    }


    /**
     * Returns sender company
     * @return \Velis\Bpm\Company
     */
    public function getCompany()
    {
        if (!isset($this->_company) && $this->company_id) {
            $this->_company = Company::instance($this->company_id);
        }
        return $this->_company;
    }


    /**
     * Loads messages sender data
     *
     * @param Message[] $messages
     * @return Person[]
     * @throws VelisException
     */
    public static function loadSendersForMessages($messages)
    {
        if (Arrays::hasColumn($messages, 'company_id')) {
            $companies = Company::instance(Arrays::getColumn($messages, 'company_id'), true);
        }
        if (Arrays::hasColumn($messages, 'person_id')) {
            $persons = Person::instance(Arrays::getColumn($messages, 'person_id'), true);
        }

        foreach ($messages as $message) {
            if ($companies[$message->company_id]) {
                $message->_company = $companies[$message->company_id];
            }
            if ($persons[$message->person_id]) {
                $message->_person  = $persons[$message->person_id];
            }
        }

        return $persons;
    }


    /**
     * Returns workflow comparable fields reflection
     * (override to extend available field set)
     *
     * @return array
     */
    public static function getWorkflowReflection()
    {
        $conditions = array(
            'sender_email' => array(
                'label'   => Lang::get('WORKFLOW_EMAIL_SENDER'),
                'type'    => Workflow::PARAM_STRING
            ),

            'subject' => array(
                'label'   => Lang::get('WORKFLOW_EMAIL_SUBJECT'),
                'type'    => Workflow::PARAM_STRING
            ),

            'body' => array(
                'label'   => Lang::get('WORKFLOW_EMAIL_BODY'),
                'type'    => Workflow::PARAM_STRING
            ),

            'email_account_id' => array(
                'label'   => Lang::get('WORKFLOW_EMAIL_ACCOUNT'),
                'options' => Account::listActive(),
            )
        );

        Workflow::sortConditions($conditions);
        return $conditions;
    }


    /**
     * Returns workflow placeholders
     *
     * @return array
     */
    public static function getWorkflowPlaceholders()
    {
        return [
            '{SenderName}' => [
                'description' => Lang::get('EMAIL_SENDER') . '(' . Lang::get('GENERAL_NAME') . ')',
            ],
            '{SenderEmail}' => [
                'description' => Lang::get('EMAIL_SENDER') . '(' . Lang::get('EMAIL_ADDRESS') . ')',
            ],
            '{Subject}' => [
                'description' => Lang::get('GENERAL_SUBJECT'),
            ],
            '{Body}' => [
                'description' => Lang::get('GENERAL_CONTENT'),
            ],
        ];
    }
}
