<?php

namespace Velis\Bpm\Ticket;

use Exception;
use Psr\SimpleCache\InvalidArgumentException;
use ReflectionException;
use User\User;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Email\Account;
use Velis\Bpm\Email\Message;
use Velis\Bpm\Person;
use Velis\Bpm\Ticket\Post\Checklist;
use Velis\Bpm\Ticket\Post\Checklist\Item as ChecklistItem;
use Velis\Bpm\Ticket\Post\File;
use Velis\Dictionary;
use Velis\Exception\BusinessLogicException;
use Velis\Filter;
use Velis\Lang;
use Velis\Model\DataObject;
use Velis\Model\Sanitizable;
use Velis\Output;
use Velis\ParameterBag;
use Velis\Policy\Bpm\Ticket\PostPolicy;
use Velis\Policy\PolicyTrait;

/**
 * Ticket post model
 * @author Olek Procki <olo@velis.pl>
 */
class Post extends DataObject implements Sanitizable
{
    use PolicyTrait;

    public const TYPE_PUBLIC       = 'Public';
    public const TYPE_PROTECTED    = 'Protected';
    public const TYPE_PRIVATE      = 'Private';

    public const ORDER_DATE        = 'date_entered';


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


    /**
     * @var File[]
     */
    protected $_files;


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


    /**
     * Email message from ticket lodgement
     * @var Message
     */
    protected $_email;


    /**
     * Post author instance
     * @var \Velis\User|Person
     */
    protected $_author;


    /**
     * Checklist
     * @var Checklist[]
     */
    protected $_checklist;


    protected static array $savedPostRecipients = [];

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


    /**
     * Returns true if user can access post contents
     *
     * @param \Velis\User|int $user
     * @return bool
     * @throws InvalidArgumentException
     */
    public function checkAccess($user = null)
    {
        if ($user == null) {
            $user = App::$user;
        } elseif (!$user instanceof \Velis\User) {
            $user = User::get($user);
        }
        if (!$this->getTicket()->checkAccess($user)) {
            return false;
        }
        return $this->isVisibleFor($user);
    }


    /**
     * Returns true if post is visible for $user (providing he can access the ticket)
     *
     * @param \Velis\Bpm\User $user
     * @return bool
     */
    public function isVisibleFor($user)
    {
        if ($this->isPublic()) {
            return true;
        }

        if ($this->isProtected()) {
            return !$user->hasCompany();
        }

        if ($this->isPrivate()) {
            if ($user->id() == $this->getAuthor()->id()) {
                return true;
            }
            return $user->hasPriv('Ticket', 'PrivatePostsAccess');
        }

        return false;
    }


    /**
     * Returns true if post is visible for logged user (providing he can access the ticket)
     * @return bool
     */
    public function isVisible()
    {
        return $this->isVisibleFor(App::$user);
    }


    /**
     * Adds new post
     *
     * @param bool $updateObjectId
     * @return Post
     * @throws Exception
     */
    public function add($updateObjectId = false)
    {
        $commit = self::$_db->startTrans();

        $checklist = null;
        if (isset($this['content'])) {
            $this['content'] = trim($this['content']);
            $checklist = new Checklist($this);
            if ($checklist->hasItems()) {
                $updateObjectId = true;
            }
            $this['content_html'] = preg_replace(
                "/(<br \/>)?\n(?![\s|])/",
                "<br />\n",
                Output::changeLinks(Filter::filterTextile($this['content']))
            );

            if (!str_starts_with($this['content_html'], '<p>')) {
                $this['content_html'] = '<p>' . $this['content_html'] . '</p>';
            }
        }

        parent::add($updateObjectId);

        try {
            if ($checklist && $checklist->hasItems()) {
                $checklist->create();
            }

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

        return $this;
    }


    /**
     * Sets post checklist
     *
     * @param Checklist[] $checklist
     * @return Post
     */
    public function setChecklist($checklist)
    {
        $this->_checklist = $checklist;

        return $this;
    }


    /**
     * Updates post data
     *
     * @param bool $checkDiff
     * @return Post
     * @throws Exception
     */
    public function modify($checkDiff = false)
    {
        if ($this->offsetExists('content')) {
            $this['content_html'] = Output::changeLinks(Filter::filterTextile($this['content']));
        }

        return parent::modify($checkDiff);
    }


    /**
     * Returns true if post is public
     * @return bool
     */
    public function isPublic()
    {
        return $this->ticket_post_visibility_id == self::TYPE_PUBLIC;
    }


    /**
     * Returns true if post is protected
     * @return bool
     */
    public function isProtected()
    {
        return $this->ticket_post_visibility_id == self::TYPE_PROTECTED;
    }


    /**
     * Returns true if post is private
     * @return bool
     */
    public function isPrivate()
    {
        return $this->ticket_post_visibility_id == self::TYPE_PRIVATE;
    }


    /**
     * Returns allowed visibility types
     * @return array
     * @throws \Velis\Exception
     */
    public static function getVisibilityTypes()
    {
        return Dictionary::get('app.ticket_post_visibility_tab');
    }


    /**
     * Return post content
     * @return string
     */
    public function getContent()
    {
        return $this['content'];
    }


    /**
     * Return post content without statuses and langs
     * @return string
     */
    public function getContentFiltered()
    {
        $content = '';

        if ($this['content']) {
            $content = $this['content'];

            $pattern = '/\[[a-zA-Z]+\]/';
            $langPattern = '/\{[A-Z\_0-9]+\}/';

            preg_match_all($pattern, $content, $matches);

            foreach ($matches[0] as $match) {
                $content = str_replace($match, '', $content);
            }

            preg_match_all($langPattern, $content, $matches);

            foreach ($matches[0] as $match) {
                $content = str_replace($match, '', $content);
            }
        }

        return $content;
    }


    /**
     * Returns post author
     * @return \Velis\User
     */
    public function getAuthor()
    {
        if (!isset($this->_author)) {
            if ($this->_hasField('user_id')) {
                $this->_author = User::bufferedInstance($this['user_id']);
            } elseif ($this->_hasField('person_id')) {
                $this->_author = Person::instance($this->person_id, true);
            }
        }
        return $this->_author;
    }


    /**
     * Sets post author
     * @param \Velis\User|Person|int $author
     * @return Post
     */
    public function setAuthor($author = null)
    {
        if ($author == null) {
            $author = App::$user;
        }
        if ($author instanceof DataObject) {
            $authorId = $author->id();
            $this->_author = $author;
        } else {
            $authorId = $author;
        }

        if ($this->_hasField('user_id')) {
            $this['user_id'] = $authorId;
        } elseif ($this->_hasField('person_id')) {
            $this['person_id'] = $authorId;
        }

        return $this;
    }


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

        return $this->_ticket;
    }

    public static function loadTicket(array $posts): array
    {
        $ids = Arrays::getColumn($posts, 'ticket_id');
        $tickets = Ticket::instance($ids);

        foreach ($posts as $post) {
            $post->_ticket = $tickets[$post->ticket_id];
        }

        return $tickets;
    }

    /**
     * Sets ticket instance
     *
     * @param Ticket $ticket
     * @return Post
     * @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;
    }


    /**
     * Connect logs to post
     *
     * @param array $logs
     * @throws Exception
     */
    public function connectLogs(array $logs)
    {
        foreach ($logs as $log) {
            if (!$log) {
                continue;
            }

            if (!($log instanceof Log)) {
                $log = new Log($log);
            }
            $log['ticket_post_id'] = $this->id();
            $log->modify();
        }
    }


    /**
     * Returns post as string
     * @return string
     */
    public function __toString()
    {
        return $this->getContent() . '';
    }


    /**
     * Returns files attached to post
     *
     * @return File[]
     * @throws ReflectionException
     */
    public function getFiles()
    {
        if (!isset($this->_files)) {
            $this->_files = Post\File::listAll(array('ticket_post_id' => $this->id()));
        }

        return $this->_files;
    }


    /**
     * Preloads ticket posts files
     *
     * @param Post[]
     * @return File[]
     */
    public static function loadPostsFiles(array $posts)
    {
        $idList = self::getCollectionIds($posts);
        $postsKeyed = array();
        $files = array();

        foreach ($posts as $post) {
            $postsKeyed[$post->id()] = $post;
            $post->_files = array();
        }
        unset($posts);

        if (count($idList)) {
            $query = "SELECT * FROM app.ticket_post_file_tab WHERE ticket_post_id IN(" . implode(',', $idList) . ") ORDER BY ticket_post_file_id DESC";

            foreach (self::$_db->cacheGetAll($query) as $row) {
                $post = $postsKeyed[$row['ticket_post_id']];
                if (($post instanceof Post) && $post->id() == $row['ticket_post_id']) {
                    $file = new File($row);
                    $file->setPost($post);
                    $post->_files[$row['ticket_post_file_id']] = $file;
                    $files[$file->id()] = $file;
                }
            }
        }

        return $files;
    }


    /**
     * Loads checklists for all elements in $posts array
     *
     * @param array $items
     * @return array
     * @throws \Velis\Exception
     * @throws DataObject\NoColumnsException
     */
    public static function loadPostsChecklists(array $items)
    {
        $idList     = Arrays::getColumn($items, 'ticket_post_id');
        $checklists = array();

        if (count($idList)) {
            $query = "SELECT * FROM app.ticket_post_checklist_tab WHERE ticket_post_id IN(" . implode(',', $idList) . ")";

            foreach (self::$_db->getAll($query) as $row) {
                $checklists[$row['ticket_post_id']][] = new ChecklistItem($row);
            }

            foreach ($items as $item) {
                if ($item instanceof Log && $item['ticket_post_id']) {
                    $post = $item->getPost();
                } elseif ($item instanceof self) {
                    $post = $item;
                } else {
                    $post = null;
                }

                if ($post) {
                    if (array_key_exists($post->id(), $checklists)) {
                        $post->setChecklist(
                            ChecklistItem::makeNestedList($checklists[$post->id()])
                        );
                    }
                }
            }
        }

        return $checklists;
    }


    /**
     * Saves post recipients
     *
     * @param array $recipients
     * @return Post
     */
    public function saveRecipients(array $recipients)
    {
        $addedRecipients = [];

        foreach ($recipients as $recipient) {
            if (!in_array($recipient->id(), $addedRecipients) && (($recipient instanceof \Velis\User && $recipient->isActive()) || $recipient instanceof Person)) {
                $params = array(
                    'ticket_post_id' => $this->id(),
                    'recipient_id' => $recipient->id()
                );

                $keyName = implode('_', array_values($params));

                if (!isset(self::$savedPostRecipients[$keyName])) {
                    self::$_db->execDML('INSERT INTO app.ticket_post_recipient_tab VALUES(:ticket_post_id, :recipient_id)', $params);

                    self::$savedPostRecipients[$keyName] = true;
                }

                $addedRecipients[] = $recipient->id();
            }
        }

        return $this;
    }

    public static function saveRecipientsForManyTickets(array $params): void
    {
        $params = array_filter($params);

        if (!$params) {
            return;
        }

        try {
            foreach ($params as $row) {
                static::$_db->insert('app.ticket_post_recipient_tab', [
                    'ticket_post_id' => $row['ticket_post_id'],
                    'person_id' => $row['recipient_id'],
                ]);
            }
        } catch (Exception $e) {
            throw new Exception('Error while saving post recipients: ' . $e->getMessage());
        }
    }

    /**
     * Returns saved post recipients
     *
     * @return Person[]|User[]
     * @throws \Velis\Exception
     * @throws InvalidArgumentException
     */
    public function getSavedRecipients()
    {
        $result = self::$_db->getAll(
            'SELECT * FROM app.ticket_post_recipient_tab WHERE ticket_post_id = :id',
            ['id' => $this->id()]
        );

        if (Arrays::hasColumn($result, 'person_id')) {
            /** @var Person[] $recipients */
            $recipients = Person::instance(Arrays::getColumn($result, 'person_id'));
        } else {
            /** @var User[] $recipients */
            $recipients = User::get(Arrays::getColumn($result, 'user_id'));
        }

        return $recipients;
    }


    /**
     * Returns related email message info
     * @return Message
     * @throws ReflectionException
     */
    public function getEmailMessage()
    {
        if (!isset($this->_email)) {
            if ($this['email_message_id']) {
                $this->_email = Message::getInstance($this['email_message_id']);
            }

            $email = Arrays::getFirst(Message::listAll([
                'ticket_post_id' => $this->id(),
                'is_pending' => 0,
            ]));

            if ($email) {
                $this->_email = $email;
            } else {
                $this->_email = false;
            }
        }

        return $this->_email;
    }


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


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

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

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

        if ($params['not_empty']) {
            self::$_listConditions[] = "COALESCE(content,'') != ''";
        }

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


    /**
     * Returns checklist items
     * @return ChecklistItem[]
     */
    public function getChecklist()
    {
        if (!isset($this->_checklist)) {
            $this->_checklist = ChecklistItem::getNestedList($this->_getPrimaryKeyParam()) ?: false;
        }

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


    /**
     * @param int|null $parentId
     * @return ChecklistItem[]
     */
    public function getChecklistItemsByParentId($parentId = null)
    {
        $items = $this->getChecklist();

        return Arrays::byValue($items, 'parent_ticket_post_checklist_id', $parentId);
    }

    /**
     * Adding new files to ticket post
     * @param array|null $files
     * @return void
     */
    public function saveFiles(?array $files)
    {
        foreach ($files as $fileData) {
            try {
                $file = new File([
                    'ticket_post_id' => $this->id(),
                    'filename'       => $fileData['name'],
                    'type'           => $fileData['type'],
                    'tmp_name'       => $fileData['tmp_name'],
                    'hash'           => md5('ticket-post-file-' . microtime()),
                    'size'           => $fileData['size'],
                    'width'          => $fileData['width'],
                    'height'         => $fileData['height']
                ]);
                $file->setPost($this);

                if ($this->_filter['versionedFileId']) {
                    $file['filename'] = str_replace($this->_filter['versionedFileId'] . '-', '', $file['filename']);

                    $versionInfo = [
                        'versionedFileId' => $this->_filter['versionedFileId'],
                        'versionComment'  => $this->_filter['fileComment'],
                    ];
                } else {
                    $versionInfo = null;
                }

                $file->add($versionInfo);
            } catch (Exception $e) {
                throw new Exception(Lang::get('GENERAL_SAVING_ERROR_FILE') . ': ' . $e->getMessage());
            }
        }
    }

    protected function getPolicy()
    {
        return new PostPolicy($this);
    }
}
