<?php

namespace App;

use App\Mailbox;
use App\SendLog;
use App\Events\ConversationStatusChanged;
use App\Events\ConversationUserChanged;
use App\Events\UserAddedNote;
use App\Events\CustomerCreatedConversation;
use App\Events\UserCreatedConversation;
use App\Events\UserCreatedConversationDraft;
use App\Events\UserCreatedThreadDraft;
use App\Events\UserReplied;
use App\Events\CustomerReplied;
use Illuminate\Database\Eloquent\Model;

class Thread extends Model
{
    /**
     * By whom action performed (source_via).
     */
    const PERSON_CUSTOMER = 1;
    const PERSON_USER = 2;

    public static $persons = [
        self::PERSON_CUSTOMER => 'customer',
        self::PERSON_USER     => 'user',
    ];

    /**
     * Thread types.
     */
    // Email from customer
    const TYPE_CUSTOMER = 1;
    // Thead created by user
    const TYPE_MESSAGE = 2;
    const TYPE_NOTE = 3;
    // Thread status change
    const TYPE_LINEITEM = 4;
    //const TYPE_PHONE = 5;
    // Forwarded threads - used in API only.
    //const TYPE_FORWARDPARENT = 6;
    //const TYPE_FORWARDCHILD = 7;
    const TYPE_CHAT = 8;

    public static $types = [
        // Thread by customer
        self::TYPE_CUSTOMER => 'customer',
        // Thread by user
        self::TYPE_MESSAGE => 'message',
        self::TYPE_NOTE    => 'note',
        // lineitem represents a change of state on the conversation. This could include, but not limited to, the conversation was assigned, the status changed, the conversation was moved from one mailbox to another, etc. A line item won’t have a body, to/cc/bcc lists, or attachments.
        self::TYPE_LINEITEM => 'lineitem',
        //self::TYPE_PHONE    => 'phone',
        // When a conversation is forwarded, a new conversation is created to represent the forwarded conversation.
        // forwardparent is the type set on the thread of the original conversation that initiated the forward event.
        //self::TYPE_FORWARDPARENT => 'forwardparent',
        // forwardchild is the type set on the first thread of the new forwarded conversation.
        //self::TYPE_FORWARDCHILD => 'forwardchild',
        // Not used.
        self::TYPE_CHAT         => 'chat',
    ];

    /**
     * Subtypes (for notes mostly)
     */
    const SUBTYPE_FORWARD = 1;
    const SUBTYPE_PHONE = 2;

    /**
     * Statuses (code must be equal to conversations statuses).
     */
    const STATUS_ACTIVE = 1;
    const STATUS_PENDING = 2;
    const STATUS_CLOSED = 3;
    const STATUS_SPAM = 4;
    const STATUS_NOCHANGE = 6;

    public static $statuses = [
        self::STATUS_ACTIVE   => 'active',
        self::STATUS_CLOSED   => 'closed',
        self::STATUS_NOCHANGE => 'nochange',
        self::STATUS_PENDING  => 'pending',
        self::STATUS_SPAM     => 'spam',
    ];

    /**
     * States.
     */
    const STATE_DRAFT = 1;
    const STATE_PUBLISHED = 2;
    const STATE_HIDDEN = 3;
    // A state of review means the thread has been stopped by Traffic Cop and is waiting
    // to be confirmed (or discarded) by the person that created the thread.
    const STATE_REVIEW = 4;

    public static $states = [
        self::STATE_DRAFT     => 'draft',
        self::STATE_PUBLISHED => 'published',
        self::STATE_HIDDEN    => 'hidden',
        self::STATE_REVIEW    => 'review',
    ];

    /**
     * Action associated with the line item.
     * It is recommended to add custom action types between 100 and 1000
     */
    // Conversation's status changed
    const ACTION_TYPE_STATUS_CHANGED = 1;
    // Conversation's assignee changed
    const ACTION_TYPE_USER_CHANGED = 2;
    // The conversation was moved from another mailbox
    const ACTION_TYPE_MOVED_FROM_MAILBOX = 3;
    // Another conversation was merged with this conversation
    const ACTION_TYPE_MERGED = 4;
    // The conversation was imported (no email notifications were sent)
    const ACTION_TYPE_IMPORTED = 5;
    //  A workflow was run on this conversation (either automatic or manual)
    // const ACTION_TYPE_WORKFLOW_MANUAL = 6;
    // const ACTION_TYPE_WORKFLOW_AUTO = 7;
    // The ticket was imported from an external Service
    const ACTION_TYPE_IMPORTED_EXTERNAL = 8;
    // Conversation customer changed
    const ACTION_TYPE_CUSTOMER_CHANGED = 9;
    // The ticket was deleted
    const ACTION_TYPE_DELETED_TICKET = 10;
    // The ticket was restored
    const ACTION_TYPE_RESTORE_TICKET = 11;

    // Describes an optional action associated with the line item
    public static $action_types = [
        self::ACTION_TYPE_STATUS_CHANGED          => 'changed-ticket-status',
        self::ACTION_TYPE_USER_CHANGED            => 'changed-ticket-assignee',
        self::ACTION_TYPE_MOVED_FROM_MAILBOX      => 'moved-from-mailbox',
        self::ACTION_TYPE_MERGED                  => 'merged',
        self::ACTION_TYPE_IMPORTED                => 'imported',
        // self::ACTION_TYPE_WORKFLOW_MANUAL         => 'manual-workflow',
        // self::ACTION_TYPE_WORKFLOW_AUTO           => 'automatic-workflow',
        self::ACTION_TYPE_IMPORTED_EXTERNAL       => 'imported-external',
        self::ACTION_TYPE_CUSTOMER_CHANGED        => 'changed-ticket-customer',
        self::ACTION_TYPE_DELETED_TICKET          => 'deleted-ticket',
        self::ACTION_TYPE_RESTORE_TICKET          => 'restore-ticket',
    ];

    /**
     * Source types (equal to thread source types).
     */
    const SOURCE_TYPE_EMAIL = 1;
    const SOURCE_TYPE_WEB = 2;
    const SOURCE_TYPE_API = 3;

    public static $source_types = [
        self::SOURCE_TYPE_EMAIL => 'email',
        self::SOURCE_TYPE_WEB   => 'web',
        self::SOURCE_TYPE_API   => 'api',
    ];

    // Metas.
    const META_CONVERSATION_HISTORY = 'ch';
    const META_PREV_CONVERSATION = 'pc';
    const META_MERGED_WITH_CONV = 'mwc';
    const META_MERGED_INTO_CONV = 'mic';
    const META_FORWARD_PARENT_CONVERSATION_NUMBER = 'fw_pcn';
    const META_FORWARD_PARENT_CONVERSATION_ID = 'fw_pci';
    const META_FORWARD_PARENT_THREAD_ID = 'fw_pti';
    const META_FORWARD_CHILD_CONVERSATION_NUMBER = 'fw_ccn';
    const META_FORWARD_CHILD_CONVERSATION_ID = 'fw_cci';

    // At some stage metas have been renamed.
    public static $meta_fw_backward_compat = [
        self::META_FORWARD_PARENT_CONVERSATION_NUMBER => 'forward_parent_conversation_number',
        self::META_FORWARD_PARENT_CONVERSATION_ID => 'forward_parent_conversation_id',
        self::META_FORWARD_PARENT_THREAD_ID => 'forward_parent_thread_id',
        self::META_FORWARD_CHILD_CONVERSATION_NUMBER => 'forward_child_conversation_number',
        self::META_FORWARD_CHILD_CONVERSATION_ID => 'forward_child_conversation_id',
    ];

    protected $dates = [
        'opened_at',
        'created_at',
        'updated_at',
        'deleted_at',
        'edited_at',
    ];

    protected $casts = [
        'meta' => 'array',
    ];

    /**
     * The user assigned to this thread (assignedTo).
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }

    /**
     * The user assigned to this thread (cached).
     */
    public function user_cached()
    {
        return $this->user()->rememberForever();
    }

    /**
     * Get the thread customer.
     */
    public function customer()
    {
        return $this->belongsTo('App\Customer');
    }

    /**
     * Get the thread customer (cached).
     */
    public function customer_cached()
    {
        return $this->customer()->rememberForever();
    }

    /**
     * Get conversation.
     */
    public function conversation()
    {
        return $this->belongsTo('App\Conversation');
    }

    /**
     * Get thread attachmets.
     */
    public function attachments()
    {
        return $this->hasMany('App\Attachment')->where('embedded', false);
        //return $this->hasMany('App\Attachment');
    }

    /**
     * Get thread embedded attachments.
     */
    public function embeds()
    {
        return $this->hasMany('App\Attachment')->where('embedded', true);
    }

    /**
     * All kinds of attachments including embedded.
     */
    public function all_attachments()
    {
        return $this->hasMany('App\Attachment');
    }

    /**
     * Get user who created the thread.
     */
    public function created_by_user()
    {
        return $this->belongsTo('App\User');
    }

    /**
     * Get user who created the thread (cached).
     */
    public function created_by_user_cached()
    {
        return $this->created_by_user()->rememberForever();
    }

    /**
     * Get customer who created the thread.
     */
    public function created_by_customer()
    {
        return $this->belongsTo('App\Customer');
    }

    /**
     * Get user who edited thread.
     */
    public function edited_by_user()
    {
        return $this->belongsTo('App\User');
    }

    /**
     * Get user who edited thread (cached).
     */
    public function edited_by_user_cached()
    {
        return $this->edited_by_user()->rememberForever();
    }

    /**
     * Get sanitized body HTML.
     *
     * @return string
     */
    public function getCleanBody($body = '')
    {
        if (!$body) {
            $body = $this->body;
        }

        if ($body === null) {
            $body = '';
        }

        // Change "background:" to "background-color:".
        // https://github.com/freescout-helpdesk/freescout/issues/2560
        // Keep in mind that with large texts preg_replace() may return null.
        $body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+[;\"'])/", '$1background-color:$2', $body) ?: $body;

        // Cut out "collapse" class as it hides elements.
        $body = preg_replace("/(<[^<>\r\n]+class=([\"'][^\"']* |[\"']))(collapse|hidden)([\"' ])/", '$1$4', $body) ?: $body;

        // Take care of MSO-comments.
        // https://github.com/freescout-help-desk/freescout/issues/5068
        // Step 1: Completely remove the MSO-specific blocks.
        // The 's' flag allows '.' to match newlines, and 'i' makes it case-insensitive.
        $body = preg_replace('/<!--\[if mso\]>.*?<!\[endif\]-->/is', '', $body);
        // Step 2: Unwrap the standard HTML from the "not mso" comments.
        // This reveals the standard <a> tag for the purifier.
        $body = preg_replace('/<!--\[if !mso\]><!--\s*-->(.*?)<!--\s*<!\[endif\]-->/is', '$1', $body);

        // Remove only the <!--[if !mso]><!--> and <!--<![endif]--> around the elements.
        // https://github.com/freescout-helpdesk/freescout/pull/3865#issuecomment-1990758149
        $body = preg_replace('/<!\-\-\[if [^>]+\]><!\-\->(.*?)<![ ]+\-\-<!\[endif\]\-\->/s', '$1', $body);

        // https://github.com/freescout-helpdesk/freescout/issues/3894
        // Remove <!--[if !mso]><!--> and <!--<![endif]--> comments, preserving the data inside
        //$body = preg_replace('/(<!\-\-\[if [^>]+\]>|<!\[endif\]\-\->)/', '', $body);

        return \Helper::purifyHtml($body);
    }

    /**
     * Convert body to plain text.
     */
    public function getBodyAsText($options = ['width' => 0])
    {
        return \Helper::htmlToText($this->body, true, $options);
    }

    public function getBodyWithFormatedLinks(string $body = '') :string
    {
        if (!$body) {
            $body = $this->body;
        }

        $body = \Helper::linkify($this->getCleanBody($body));

        // Add target="_blank" to links.
        $pattern = '/<a(.*?)?href=[\'"]?[\'"]?(.*?)?>/i';

        $body = preg_replace_callback($pattern, function ($m) {
            $tpl = array_shift($m);
            $href = isset($m[1]) ? $m[1] : null;

            if (preg_match('/target=[\'"]?(.*?)[\'"]?/i', $tpl)) {
                return $tpl;
            }

            if (trim($href) && 0 === strpos($href, '#')) {
                // Anchor links.
                return $tpl;
            }

            return preg_replace_callback('/href=/i', function ($m2) {
                return sprintf('target="_blank" %s', array_shift($m2));
            }, $tpl);

        }, $body) ?: $body;

        return $body;
    }

    /**
     * Get sanitized body HTML.
     *
     * @return string
     */
    public function getCleanBodyOriginal()
    {
        return $this->getCleanBody($this->body_original);
    }

    /**
     * Get thread recipients.
     *
     * @return array
     */
    public function getToArray($exclude_array = [])
    {
        return \App\Misc\Helper::jsonToArray($this->to, $exclude_array);
    }

    public function getToString($exclude_array = [])
    {
        return implode(', ', $this->getToArray($exclude_array));
    }

    /**
     * Get first address from the To list.
     */
    public function getToFirst()
    {
        $to = $this->getToArray();

        return array_shift($to);
    }

    /**
     * Get type name.
     */
    public function getTypeName()
    {
        return self::$types[$this->type];
    }

    /**
     * Get thread CC recipients.
     *
     * @return array
     */
    public function getCcArray($exclude_array = [])
    {
        return \App\Misc\Helper::jsonToArray($this->cc, $exclude_array);
    }

    public function getCcString($exclude_array = [])
    {
        return implode(', ', $this->getCcArray($exclude_array));
    }

    /**
     * Get thread BCC recipients.
     *
     * @return array
     */
    public function getBccArray($exclude_array = [])
    {
        return \App\Misc\Helper::jsonToArray($this->bcc, $exclude_array);
    }

    public function getBccString($exclude_array = [])
    {
        return implode(', ', $this->getBccArray($exclude_array));
    }

    /**
     * Set to as JSON.
     */
    public function setTo($emails)
    {
        $emails_array = Conversation::sanitizeEmails($emails);
        if ($emails_array) {
            $emails_array = array_unique($emails_array);
            $this->to = \Helper::jsonEncodeUtf8($emails_array);
        } else {
            $this->to = null;
        }
    }

    public function setCc($emails)
    {
        $emails_array = Conversation::sanitizeEmails($emails);
        if ($emails_array) {
            $emails_array = array_unique($emails_array);
            $this->cc = \Helper::jsonEncodeUtf8($emails_array);
        } else {
            $this->cc = null;
        }
    }

    public function setBcc($emails)
    {
        $emails_array = Conversation::sanitizeEmails($emails);
        if ($emails_array) {
            $emails_array = array_unique($emails_array);
            $this->bcc = \Helper::jsonEncodeUtf8($emails_array);
        } else {
            $this->bcc = null;
        }
    }

    /**
     * Get thread's status name.
     *
     * @return string
     */
    public function getStatusName()
    {
        return self::statusCodeToName($this->status);
    }

    /**
     * Get status name. Made as a function to allow status names translation.
     *
     * @param int $status
     *
     * @return string
     */
    public static function statusCodeToName($status)
    {
        switch ($status) {
            case self::STATUS_ACTIVE:
                return __('Active');
                break;

            case self::STATUS_PENDING:
                return __('Pending');
                break;

            case self::STATUS_CLOSED:
                return __('Closed');
                break;

            case self::STATUS_SPAM:
                return __('Spam');
                break;

            case self::STATUS_NOCHANGE:
                return __('Not changed');
                break;

            default:
                return '';
                break;
        }
    }

    /**
     * Get text for the assignee in line item.
     *
     * @return string
     */
    public function getAssigneeName($ucfirst = false, $by_user = null)
    {
        if (!$by_user) {
            $by_user = auth()->user();
        }
        if (!$this->user_id) {
            if ($ucfirst) {
                return __('Anyone');
            } else {
                return __('anyone');
            }
        } elseif ($by_user && $this->user_id == $by_user->id) {
            if ($this->created_by_user_id && $this->created_by_user_id == $this->user_id) {
                $name = __('yourself');
            } else {
                $name = __('you');
            }
            if ($ucfirst) {
                $name = ucfirst($name);
            }

            return $name;
        } else {
            // User may be deleted
            if ($this->user) {
                return $this->user->getFullName();
            } else {
                return '';
            }
        }
    }

    /**
     * Get user or customer who created the thead.
     */
    public function getCreatedBy()
    {
        if (!empty($this->created_by_user_id)) {
            // User can be deleted
            if ($this->created_by_user) {
                return $this->created_by_user;
            } else {
                return \App\User::getDeletedUser();
            }
        } else {
            // In some cases the created_by_customer can be empty.
            if ($this->created_by_customer) {
                return $this->created_by_customer;
            } else {
                return \App\Customer::getDummyCustomer();
            }
        }
    }

    /**
     * Get creator of the thread.
     */
    public function getPerson($cached = false)
    {
        if ($this->type == self::TYPE_CUSTOMER) {
            if ($cached) {
                return $this->customer_cached;
            } else {
                return $this->customer;
            }
        } else {
            if ($cached) {
                return $this->created_by_user_cached;
            } else {
                return $this->created_by_user;
            }
        }
    }

    /**
     * Get action's person.
     */
    public function getActionPerson($conversation_number = '')
    {
        $person = '';

        if ($this->type == self::TYPE_CUSTOMER) {
            if ($this->customer_cached) {
                $person = $this->customer_cached->getFullName(true);
            }
        } elseif ($this->state == self::STATE_DRAFT && !empty($this->edited_by_user_id)) {
            // Draft
            if (auth()->user() && $this->edited_by_user_id == auth()->user()->id) {
                $person = __('you');
            } else {
                $person = $this->edited_by_user->getFullName();
            }
        } elseif ($this->created_by_user_cached) {
            if ($this->created_by_user_id && auth()->user() && $this->created_by_user_cached->id == auth()->user()->id) {
                $person = __('you');
            } else {
                $person = $this->created_by_user_cached->getFullName();
            }
        }

        // https://github.com/tormjens/eventy/issues/19
        $person = \Eventy::filter('thread.action_person', $person, $this, $conversation_number);

        return $person;
    }

    /**
     * Get action text.
     * $by_user - user who performed the action.
     * $person must be already escaped.
     */
    public function getActionText($conversation_number = '', $escape = false, $strip_tags = false, $by_user = null, $person = '', $viewed_by_user = null)
    {
        $did_this = '';

        // Did this
        if ($this->type == self::TYPE_LINEITEM) {

            if ($this->action_type == self::ACTION_TYPE_STATUS_CHANGED) {
                if ($conversation_number) {
                    $did_this = __(':person marked as :status_name conversation #:conversation_number', ['status_name' => $this->getStatusName(), 'conversation_number' => $conversation_number]);
                } else {
                    $did_this = __(":person marked as :status_name", ['status_name' => $this->getStatusName()]);
                }
            } elseif ($this->action_type == self::ACTION_TYPE_USER_CHANGED) {
                $assignee = $this->getAssigneeName(false, $by_user);
                if ($escape) {
                    $assignee = htmlspecialchars($assignee);
                }
                if ($conversation_number) {
                    $did_this = __(':person assigned :assignee conversation #:conversation_number', ['assignee' => $assignee, 'conversation_number' => $conversation_number]);
                } else {
                    $did_this = __(":person assigned to :assignee", ['assignee' => $assignee]);
                }
            } elseif ($this->action_type == self::ACTION_TYPE_CUSTOMER_CHANGED) {
                if ($conversation_number) {
                    $did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => ($this->customer_cached ? $this->customer_cached->getFullName(true) : ''), 'conversation_number' => $conversation_number]);
                } else {
                    $customer_name = '';
                    if ($this->customer_cached) {
                        $customer_name = $this->customer_cached->getFullName(true);
                    }
                    if ($escape) {
                        $customer_name = htmlspecialchars($customer_name);
                    }
                    $did_this = __(":person changed the customer to :customer", ['customer' => '<a href="'.($this->customer_cached ? $this->customer_cached->url() : '').'" title="'.$this->action_data.'" class="link-black">'.$customer_name.'</a>']);
                }
            } elseif ($this->action_type == self::ACTION_TYPE_DELETED_TICKET) {
                $did_this = __(":person deleted");
            } elseif ($this->action_type == self::ACTION_TYPE_RESTORE_TICKET) {
                $did_this = __(":person restored");
            } elseif ($this->action_type == self::ACTION_TYPE_MOVED_FROM_MAILBOX) {
                $did_this = __(":person moved conversation from another mailbox");
            } elseif ($this->action_type == self::ACTION_TYPE_MERGED) {
                if (!empty($this->getMeta(Thread::META_MERGED_WITH_CONV))) {
                    $did_this = __(":person merged with another conversation");
                } else {
                    $merge_conversation = Conversation::find($this->getMeta(Thread::META_MERGED_INTO_CONV));
                    $merge_conversation_number = '';
                    if ($merge_conversation) {
                        $merge_conversation_number = $merge_conversation->number;
                    }
                    if ($merge_conversation) {
                        $did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => '<a href="'.$merge_conversation->url().'" class="link-black">'.$merge_conversation_number.'</a>']);
                    } else {
                        $did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => $merge_conversation_number]);
                    }
                }
            }
        } elseif ($this->state == self::STATE_DRAFT) {
            if (empty($this->edited_by_user_id)) {
                $did_this = __(':person created a draft');
            } else {
                $did_this = __(":person edited :creator's draft", ['creator' => $this->created_by_user_cached->getFirstName()]);
            }
        } else {
            if ($this->isForwarded()) {
                $did_this = __(':person forwarded a conversation #:forward_parent_conversation_number', ['forward_parent_conversation_number' => $this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_NUMBER)]);
            } elseif ($this->first) {
                $did_this = __(':person started a new conversation #:conversation_number', ['conversation_number' => $conversation_number]);
            } elseif ($this->type == self::TYPE_NOTE) {
                $did_this = __(':person added a note to conversation #:conversation_number', ['conversation_number' => $conversation_number]);
            } else {
                $did_this = __(':person replied to conversation #:conversation_number', ['conversation_number' => $conversation_number]);
            }
        }

        $did_this = \Eventy::filter('thread.action_text', $did_this, $this, $conversation_number, $escape, $viewed_by_user);

        if ($strip_tags) {
            $did_this = strip_tags($did_this);
        }

        if ($person) {
            // This causes double escaping.
            // if ($escape) {
            //     $person = htmlspecialchars($person);
            // }
            $did_this = str_replace(':person', $person, $did_this);
        }

        return $did_this;
    }

    /**
     * Description of what happened.
     */
    public function getActionDescription($conversation_number, $escape = true, $viewed_by_user = null)
    {
        // Person
        $person = $this->getActionPerson($conversation_number);
        $did_this = $this->getActionText($conversation_number, false, false, null, '', $viewed_by_user);

        if ($escape) {
            $person = htmlspecialchars($person);
            $did_this = htmlspecialchars($did_this);
        }

        return __($did_this, [
            'person'           => '<strong>'.$person.'</strong>',
            'did_this'         => $did_this,
        ]);
    }

    /**
     * Get thread state name.
     */
    public function getStateName()
    {
        return self::$states[$this->state];
    }

    public function deleteThread()
    {
        $this->deteleAttachments();
        $this->delete();

        if ($this->isNote()) {
            Conversation::updatePreview($this->conversation_id);
        }
    }

    /**
     * Delete thread attachments.
     */
    public function deteleAttachments()
    {
        Attachment::deleteByIds($this->all_attachments()->pluck('id')->toArray());
    }

    public function isDraft()
    {
        return $this->state == self::STATE_DRAFT;
    }

    /**
     * Get original body or body.
     */
    public function getBodyOriginal()
    {
        if (!empty($this->body_original)) {
            return $this->body_original;
        } else {
            return $this->body;
        }
    }

    /**
     * Get name for the reply to customer.
     *
     * @param [type] $mailbox [description]
     *
     * @return [type] [description]
     */
    public function getFromName($mailbox = null)
    {
        // Created by customer
        if ($this->source_via == self::PERSON_CUSTOMER) {
            if ($this->getCreatedBy()) {
                return $this->getCreatedBy()->getFirstName(true);
            } else {
                return '';
            }
        }

        // Created by user
        if (empty($mailbox)) {
            $mailbox = $this->conversation->mailbox;
        }
        // Mailbox name by default
        $name = $mailbox->name;

        if ($mailbox->from_name == Mailbox::FROM_NAME_CUSTOM && $mailbox->from_name_custom) {
            $data = [
                'mailbox' => $mailbox,
                'mailbox_from_name' => '', // To avoid recursion.
                'conversation' => $this->conversation,
                // If we reach here it means the thread has been created by user.
                'user' => $this->getCreatedBy(),
            ];
            $name = \MailHelper::replaceMailVars($mailbox->from_name_custom, $data, false, true);
        } elseif ($mailbox->from_name == Mailbox::FROM_NAME_USER && $this->getCreatedBy()) {
            $name = $this->getCreatedBy()->getFirstName(true);
        }

        return $name;
    }

    /**
     * Check if thread is a reply from customer or user.
     *
     * @return bool [description]
     */
    public function isReply()
    {
        return in_array($this->type, [\App\Thread::TYPE_MESSAGE, \App\Thread::TYPE_CUSTOMER]);
    }

    /**
     * Is this thread created from auto responder email.
     *
     * @return bool [description]
     */
    public function isAutoResponder()
    {
        return \MailHelper::isAutoResponder($this->headers);
    }

    /**
     * Is thread created from incoming bounce email.
     *
     * @return bool [description]
     */
    public function isBounce()
    {
        if (!empty($this->getSendStatusData()['is_bounce'])) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Send status data mayb contain the following information:
     * - bounce info (status_code, action, diagnostic_code, is_bounce, bounce_for_thread, bounce_for_conversation, bounced_by_thread, bounced_by_conversation)
     * - send error message
     * - click date
     * - unsubscribe date
     * - complain date.
     *
     * @return [type] [description]
     */
    public function getSendStatusData()
    {
        return \Helper::jsonToArray($this->send_status_data);
    }

    public function updateSendStatusData($new_data)
    {
        if ($new_data) {
            $send_status_data = $this->getSendStatusData();
            if ($send_status_data) {
                $send_status_data = array_merge($send_status_data, $new_data);
            } else {
                $send_status_data = $new_data;
            }
            if (!empty($send_status_data['msg'])) {
                $send_status_data['msg'] = \MailHelper::sanitizeSmtpStatusMessage($send_status_data['msg']);
            }
            $this->send_status_data = \Helper::jsonEncodeUtf8($send_status_data);
        } else {
            $this->send_status_data = null;
        }
    }

    public function isSendStatusError()
    {
        return in_array($this->send_status, \App\SendLog::$status_errors);
    }

    /**
     * Create thread.
     *
     * @param  [type] $conversation_id [description]
     * @param  [type] $text            [description]
     * @param  array  $data            [description]
     * @return [type]                  [description]
     */
    public static function create($conversation, $type, $body, $data = [], $save = true)
    {
        $thread = new Thread();
        $thread->conversation_id = $conversation->id;
        $thread->type = $type;
        $thread->body = $body;
        $thread->status = $conversation->status;
        $thread->state = Thread::STATE_PUBLISHED;

        // Assigned to.
        if (!empty($data['user_id'])) {
            $thread->user_id = $data['user_id'];
        }
        if (!empty($data['message_id'])) {
            $thread->message_id = $data['message_id'];
        }
        if (!empty($data['headers'])) {
            $thread->headers = $data['headers'];
        }
        if (!empty($data['from'])) {
            $thread->from = $data['from'];
        }
        if (!empty($data['to'])) {
            $thread->setTo($data['to']);
        }
        if (!empty($data['cc'])) {
            $thread->setCc($data['cc']);
        }
        if (!empty($data['bcc'])) {
            $thread->setBcc($data['bcc']);
        }
        if (isset($data['first'])) {
            $thread->from = $data['first'];
        }
        if (isset($data['source_via'])) {
            $thread->source_via = $data['source_via'];
        }
        if (isset($data['source_type'])) {
            $thread->source_type = $data['source_type'];
        }
        if (!empty($data['customer_id'])) {
            $thread->customer_id = $data['customer_id'];
        }
        if (!empty($data['created_by_customer_id'])) {
            $thread->created_by_customer_id = $data['created_by_customer_id'];
        }
        if (!empty($data['created_by_user_id'])) {
            $thread->created_by_user_id = $data['created_by_user_id'];
        }
        if (!empty($data['action_type'])) {
            $thread->action_type = $data['action_type'];
        }
        if (!empty($data['meta'])) {
            $thread->setMetas($data['meta']);
        }

        if ($save) {
            $thread->save();
        }

        return $thread;
    }

    public static function createExtended($data = [], $conversation = null, $customer = null, $update_conv = true)
    {
        if (empty($data['type']) || empty($data['body'])) {
            return false;
        }

        $is_customer = ($data['type'] == Thread::TYPE_CUSTOMER);

        if (!$customer && !empty($data['customer_id'])) {
            $customer = Customer::find($data['customer_id']);
        }
        if (!$customer) {
            $customer = $conversation->customer;
        }

        // User which creatd the thread should be passed in created_by_user_id.
        // user_id is check for backward compatibility.
        $user_id = $data['created_by_user_id'] ?? $data['user_id'] ?? null;

        // Check type.
        if ($data['type'] == Thread::TYPE_CUSTOMER && empty($customer)) {
            return false;
            //return $this->getErrorResponse('`customer` parameter is required', 'customer');
        }
        if (($data['type'] == Thread::TYPE_MESSAGE || $data['type'] == Thread::TYPE_NOTE) && empty($user_id)) {
            return false;
            //return $this->getErrorResponse('`user` parameter is required', 'user');
        }

        // Create thread.
        $now = date('Y-m-d H:i:s');

        // New conversation.
        $new = !$conversation->threads_count;

        $thread = new Thread();
        $thread->conversation_id = $conversation->id;
        $thread->type = $data['type'];
        if ($is_customer) {
            $thread->source_via = Thread::PERSON_CUSTOMER;
            $thread->created_by_customer_id = $customer->id;
        } else {
            $thread->source_via = Thread::PERSON_USER;
            $thread->created_by_user_id = $user_id;
            $thread->edited_by_user_id = null;
            $thread->edited_at = null;
        }
        $thread->source_type = Thread::SOURCE_TYPE_API;
        $thread->state = $data['state'] ?? Thread::STATE_PUBLISHED;
        $thread->customer_id = $customer->id ?? $conversation->customer_id ?? null;
        $thread->body = $data['body'];
        if (!$is_customer) {
            $thread->setTo([$customer->getMainEmail()]);
        }
        
        $cc = \MailHelper::sanitizeEmails($data['cc'] ?? []);
        $thread->setCc($cc);

        $bcc = \MailHelper::sanitizeEmails($data['bcc'] ?? []);
        $thread->setBcc($bcc);
        $thread->imported = (int)($data['imported'] ?? false);
        if ($thread->imported && !empty($data['created_at'])) {
            $thread->created_at = self::utcStringToServerDate($data['created_at']);
        }
        if ($new) {
            $thread->first = true;
        }

        // Assignee.
        if (empty($data['user_id'])) {
            $thread->user_id = $conversation->user_id;
        }

        // Process attachments.
        if (!empty($data['attachments'])) {
            $has_attachments = false;
            foreach ($data['attachments'] as $attachment) {

                $content = null;
                $uploaded_file = null;

                if (is_object($attachment) && get_class($attachment) == 'Illuminate\Http\UploadedFile') {

                    $uploaded_file = $attachment;
                    $attachment = [];
                    $attachment['file_name'] = $uploaded_file->getClientOriginalName();
                    $attachment['mime_type'] = $uploaded_file->getMimeType();

                } else {

                    if (empty($attachment['file_name'])
                        //|| empty($attachment['mime_type'])
                        || (empty($attachment['data']) && empty($attachment['file_url']))
                    ) {
                        continue;
                    }

                    if (!empty($attachment['data'])) {
                        // BASE64 string.
                        $content = base64_decode($attachment['data']);
                        if (!$content) {
                            continue;
                        }
                        if (empty($attachment['mime_type'])) {
                            $f = finfo_open();
                            $attachment['mime_type'] = finfo_buffer($f, $content, FILEINFO_MIME_TYPE);
                        }
                    } else {
                        // URL.
                        $file_path = \Helper::downloadRemoteFileAsTmp($attachment['file_url']);
                        if (!$file_path) {
                            continue;
                        }
                        $uploaded_file = new \Illuminate\Http\UploadedFile(
                            $file_path, basename($file_path),
                            null, null, true
                        );
                        if (empty($attachment['mime_type'])) {
                            $attachment['mime_type'] = mime_content_type($file_path);
                            if (empty($attachment['mime_type'])) {
                                $attachment['mime_type'] = $uploaded_file->getMimeType();
                            }
                        }
                    }
                }
                if (!$has_attachments) {
                    $thread->save();
                }
                $attachment = Attachment::create(
                    $attachment['file_name'],
                    $attachment['mime_type'],
                    null,
                    $content,
                    $uploaded_file,
                    $embedded = false,
                    $thread->id,
                    $user_id ?? null,
                    $data['attachments_upload_mode'] ?? \Helper::UPLOAD_MODE_DEFAULT,
                );

                if ($attachment) {
                    $has_attachments = true;
                }
            }
            if ($has_attachments) {
                $thread->has_attachments = true;
                $conversation->has_attachments = true;
            }
        } else {
            $has_attachments = $data['has_attachments'] ?? false;
            $thread->has_attachments = $has_attachments;
            $conversation->has_attachments = $has_attachments;
        }
        
        $thread->save();
        
        if ($new) {
            if ($is_customer) {
                $conversation->source_via = Conversation::PERSON_CUSTOMER;
                $conversation->created_by_customer_id = $customer->id;
            } else {
                $conversation->source_via = Conversation::PERSON_USER;
                $conversation->created_by_user_id = $user_id;
            }
        }

        $conversation->setCc($cc);
        // BCC should keep BCC of the first email,
        // so we change BCC only if it contains emails.
        if ($bcc) {
            $conversation->setBcc($bcc);
        }

        $update_folder = false;

        if ($thread->isReply()) {
            $conversation->last_reply_at = $now;
            if ($is_customer) {
                $conversation->last_reply_from = Conversation::PERSON_CUSTOMER;

                // Set specific status
                if (!empty($data['status'])) {
                    if ((int)$conversation->status != (int)$data['status']) {
                        $update_folder = true;
                    }
                    $conversation->status = $data['status'];
                } else {
                    if ((int)$conversation->status != Conversation::STATUS_ACTIVE) {
                        $update_folder = true;
                    }
                    // Reply from customer makes conversation active
                    $conversation->status = Conversation::STATUS_ACTIVE;
                }
            } else {
                $conversation->last_reply_from = Conversation::PERSON_USER;
                $conversation->user_updated_at = $now;
                
                if (!empty($data['status'])) {
                    if ((int)$conversation->status != (int)$data['status']) {
                        $update_folder = true;
                    }
                    $conversation->status = $data['status'];
                } else {
                    if ((int)$conversation->status != Conversation::STATUS_PENDING) {
                        $update_folder = true;
                    }
                    // Reply from customer makes conversation active
                    $conversation->status = Conversation::STATUS_PENDING;
                }
            }
        }
        
        // Reply from customer to deleted conversation should undelete it.
        if ($data['type'] == Thread::TYPE_CUSTOMER && $conversation->state == Conversation::STATE_DELETED) {
            $conversation->state = Conversation::STATE_PUBLISHED;
            $update_folder = true;
        }

        if ($update_conv) {
            $conversation->customer_id = $customer->id;

            if ($is_customer) {
                $conversation->customer_email = $customer->getMainEmail();
            }

            if ($update_folder) {
                $conversation->updateFolder();
            }
        }

        // Update conversation here if needed.
        if ($is_customer) {
            if ($new) {
                $conversation = \Eventy::filter('conversation.created_by_customer', $conversation, $thread, $customer);
            } else {
                $conversation = \Eventy::filter('conversation.customer_replied', $conversation, $thread, $customer);
            }
        }
        // save() will check if something in the model has changed. If it hasn't it won't run a db query.
        $conversation->save();

        // Update folders counters
        if (!$new) {
            // Update folders counters
            $conversation->mailbox->updateFoldersCounters();
        }

        // Events.

        // Conversation customer changed
        // Not used anywhere
        // if ($prev_customer_id) {
        //     event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
        // }
    
        if ($new) {
            if ($is_customer) {
                event(new CustomerCreatedConversation($conversation, $thread));
                \Eventy::action('conversation.created_by_customer', $conversation, $thread, $customer);
            } else {
                // New conversation.
                event(new UserCreatedConversation($conversation, $thread));
                \Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
                // After Conversation::UNDO_TIMOUT period trigger final event.
                \Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
            }
        } elseif ($data['type'] == Thread::TYPE_NOTE) {
            // Note.
            event(new UserAddedNote($conversation, $thread));
            \Eventy::action('conversation.note_added', $conversation, $thread);
        } else {
            // Reply.
            if ($is_customer) {
                event(new CustomerReplied($conversation, $thread));
                \Eventy::action('conversation.customer_replied', $conversation, $thread, $customer);
            } else {
                event(new UserReplied($conversation, $thread));
                \Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
                // After Conversation::UNDO_TIMOUT period trigger final event.
                \Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
            }
        }

        return $thread;
    }

    /**
     * Get full name of the user who edited thread.
     */
    public function getEditedByUserName()
    {
        $name = '';

        if (!$this->edited_by_user_id) {
            return '';
        }

        if (auth()->user() && $this->edited_by_user_id == auth()->user()->id) {
            $name = __('you');
        } else {
            $name = $this->edited_by_user_cached->getFullName();
        }

        return $name;
    }

    /**
     * Get thread meta data as array.
     */
    public function getMetas()
    {
        return $this->meta;
        //return \Helper::jsonToArray($this->meta);
    }

    /**
     * Set thread meta value.
     */
    public function setMetas($data)
    {
        $this->meta = $data;
        //$this->meta = \Helper::jsonEncodeUtf8($data);
    }

    /**
     * Get thread meta value.
     */
    public function getMeta($key, $default = null)
    {
        $metas = $this->getMetas();
        if (isset($metas[$key])) {
            return $metas[$key];
        } else {
            return $default;
        }
    }

    /**
     * Set thread meta value.
     */
    public function setMeta($key, $value)
    {
        $metas = $this->getMetas();
        $metas[$key] = $value;
        $this->setMetas($metas);
    }

    public function getMetaFw($key, $default = null)
    {
        $meta = $this->getMeta($key, $default);
        if (!$meta) {
            $meta = $this->getMeta(self::$meta_fw_backward_compat[$key], $default);
        }
        return $meta;
    }

    /**
     * Unset thread meta value.
     */
    public function unsetMeta($key)
    {
        $metas = $this->getMetas();
        if (isset($metas[$key])) {
            unset($metas[$key]);
            $this->setMetas($metas);
        }
    }

    /**
     * Get full name of the user who forwarded conversation.
     */
    public function getForwardByFullName($by_user = null)
    {
        if (!$by_user) {
            $by_user = auth()->user();
        }
        if ($by_user && $this->created_by_user_id == $by_user->id) {
            $name = __('you');
        } elseif ($this->created_by_user) {
            $name = $this->created_by_user->getFullName();
        } else {
            $name = '';
        }

        return $name;
    }

    /**
     * Is this a note informing that conversation has been forwarded.
     */
    public function isForward()
    {
        return ($this->subtype == \App\Thread::SUBTYPE_FORWARD);
    }

    /**
     * Is this a forwarded conversation.
     */
    public function isForwarded()
    {
        if ($this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_ID)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Is this thread a note.
     */
    public function isNote()
    {
        return ($this->type == \App\Thread::TYPE_NOTE);
    }

    /**
     * Get forwarded conversation.
     */
    public function getForwardParentConversation()
    {
        return Conversation::where('id', $this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_ID))
            ->rememberForever()
            ->first();
    }

    /**
     * Get forward child conversation.
     */
    public function getForwardChildConversation()
    {
        return Conversation::where('id', $this->getMetaFw(self::META_FORWARD_CHILD_CONVERSATION_ID))
            ->first();
    }

    /**
     * Fetch body via IMAP.
     */
    public function fetchBody()
    {
        $mailbox = null;

        // The conversation may has been moved from another mailbox.
        if (!empty($this->conversation->meta['orig_mailbox_id'])) {
            $mailbox = Mailbox::find($this->conversation->meta['orig_mailbox_id']);
        }

        if (!$mailbox) {
            $mailbox = $this->conversation->mailbox;
        }
        $message = \MailHelper::fetchMessage($mailbox, $this->message_id, $this->getMailDate());

        // Try without limiting by date.
        // https://github.com/freescout-helpdesk/freescout/issues/3658
        if (!$message) {
            $message = \MailHelper::fetchMessage($mailbox, $this->message_id);
        }

        if (!$message) {
            return '';
        }

        $body = $message->getHTMLBody();

        if (!$body) {
            $body = $message->getTextBody();
        }

        return $body;
    }

    public function parseHeaders()
    {
        return \MailHelper::parseHeaders($this->headers);
    }

    public function getHeader($header_name)
    {
        return \MailHelper::getHeader($this->headers, $header_name);
    }

    public function getFromHeader()
    {
        if (empty($this->headers)) {
            return '';
        }
        preg_match("#From:\s*.*[^\s]*\s*<\s*(.*[^\s])\s*>\s*\n#", $this->headers ?? '', $m);
        return $m[1] ?? '';
    }

    public function getMailDate()
    {
        $data = $this->parseHeaders();

        if (empty($data->date)) {
            return null;
        }

        return \Helper::parseDateToCarbon($data->date);
    }

    public function getActionTypeName()
    {
        if (!$this->action_type) {
            return '';
        }

        $action_types = \Eventy::filter('thread.action_types', self::$action_types);

        return self::$action_types[$this->action_type] ?? '';
    }

    public function isCustomerMessage()
    {
        return $this->type == self::TYPE_CUSTOMER;
    }

    public function isUserMessage()
    {
        return $this->type == self::TYPE_MESSAGE;
    }

    public static function replaceBase64ImagesWithAttachments($body, $user_id = null)
    {
        \Helper::setPcreBacktrackLimit();

        $body = preg_replace_callback(
            "#(<img[^<>]+src=[\"'])data:image/([^;]+);base64,([^\"']+)([\"'])#",
            function ($match) {
                $attachment = null;
                $data = base64_decode($match[3]);

                if ($data) {
                    $attachment = Attachment::create(
                        $file_name = number_format(microtime(true), 4, '', '').'.'.$match[2],
                        $mime_type = 'image/'.$match[2],
                        $type = Attachment::TYPE_IMAGE,
                        $data,
                        $uploaded_file = null,
                        $embedded = true,
                        $thread_id = null,
                        $user_id = \Auth::id()
                    );
                }
                if ($attachment) {
                    return $match[1].$attachment->url().$match[4];
                } else {
                    return $match[0];
                }
            }, $body
        );

        return $body;
    }

    public function getMessageId($mailbox = null)
    {
        if ($this->isCustomerMessage() && $this->message_id) {
            return $this->message_id;
        }
        if ($this->isUserMessage()) {
            if (!$mailbox) {
                $mailbox = $this->conversation->mailbox;
            }
            return \MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->id.'-'.\MailHelper::getMessageIdHash($this->id).'@'.$mailbox->getEmailDomain();
        }

        return '';
    }

    // Sorts threads in desc order by created_at and ID.
    // 
    // Threads has to be sorted by created_at and not by id.
    // https://github.com/freescout-helpdesk/freescout/issues/2938
    // Sometimes thread.created_at may be the same,
    // in such cases we also need to sort by thread ID.
    public static function sortThreads($threads)
    {
        return $threads->sort(function ($a, $b) {
            $a_ts = $a->created_at->getTimestamp();
            $b_ts = $b->created_at->getTimestamp();
            if ($a_ts == $b_ts) {
                if ($a->id < $b->id) {
                    return 1;
                } else {
                    return -1;
                }
            } else {
                return ($a_ts < $b_ts) ? 1 : -1;
            }
        });
    }

    public static function getLastThread($threads)
    {
        $threads = self::sortThreads($threads);
        return $threads->first();
    }

    public function canRetrySend()
    {
        if ($this->isSendStatusSuccess()) {
            return false;
        }
        // Check if failed_job still exists.
        if (!$this->getFailedJobId()) {
            return false;
        }

        return true;
    }

    public function isSendStatusSuccess()
    {
        // We have not tried to send the email yet.
        if ((int)$this->send_status == 0) {
            return false;
        }

        if (!in_array($this->send_status, [SendLog::STATUS_SEND_ERROR, SendLog::STATUS_DELIVERY_ERROR, SendLog::STATUS_SEND_INTERMEDIATE_ERROR])) {
            return true;
        }
        
        return false;
    }

    public function getFailedJobId()
    {
        return \App\FailedJob::where('queue', 'emails')
            ->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%{i:0;i:'.$this->id.';%')
            ->value('id');
    }
}
