<?php

namespace App\Jobs;

use App\Mail\UserNotification;
use App\SendLog;
use App\Thread;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Mail;

class SendNotificationToUsers implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $users;

    public $conversation;

    public $threads;

    // Max retries + 1
    public $tries = 168; // One per hour

    /**
     * The number of seconds the job can run before timing out.
     * fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
     * in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
     * So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
     */
    public $timeout = 120;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($users, $conversation, $threads)
    {
        $this->users = $users;
        $this->conversation = $conversation;
        $this->threads = $threads;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $mailbox = $this->conversation->mailbox;

        // Configure mail driver according to Mailbox settings
        \App\Misc\Mail::setMailDriver($mailbox, null, $this->conversation);

        // Threads has to be sorted here, if sorted before, they come here in wrong order
        $this->threads = Thread::sortThreads($this->threads);

        $headers = [];
        $last_thread = $this->threads->first();

        if (!$last_thread) {
            return;
        }

        // If thread is draft, it means it has been undone
        if ($last_thread->isDraft()) {
            return;
        }

        // Do not send email notifications to support agents
        // if the email is a bounce and mail server send limit reached.
        // https://github.com/freescout-help-desk/freescout/issues/4806
        if ($last_thread->isBounce() && strstr($last_thread->body, 'message limit exceeded')) {
            return;
        }

        // Limit conversation history
        if (config('app.email_user_history') == 'last') {
            $this->threads = $this->threads->slice(0, 2);
        }

        if (config('app.email_user_history') == 'none') {
            $this->threads = $this->threads->slice(0, 1);
        }

        // All notification for the same conversation has same dummy Message-ID
        $prev_message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY.'-'.$this->conversation->id.'-'.md5($this->conversation->id).'@'.$mailbox->getEmailDomain();
        $headers['In-Reply-To'] = '<'.$prev_message_id.'>';
        $headers['References'] = '<'.$prev_message_id.'>';
        // https://github.com/freescout-helpdesk/freescout/issues/2488
        $headers['X-Auto-Response-Suppress'] = 'All';

        // We throw an exception if any of the send attempts throws an exception (connection error, etc)
        $last_send_exception = null;

        foreach ($this->users as $user) {

            // User can ne deleted from DB.
            if (!isset($user->id)) {
                continue;
            }

            if ($user->isDeleted()) {
                continue;
            }

            // If for one user sending fails the job is marked as failed and retried after some time.
            // So we have to check if notification email has already been successfully sent to this user.
            if ($this->attempts() > 1) {
                // Maybe add indexes to the table.
                $already_sent = SendLog::where('thread_id', $last_thread->id)
                    ->where('mail_type', SendLog::MAIL_TYPE_USER_NOTIFICATION)
                    ->where('user_id', $user->id)
                    ->whereIn('status', SendLog::$sent_success)
                    ->exists();
                if ($already_sent) {
                    continue;
                }
            }

            $message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION.'-'.$last_thread->id.'-'.$user->id.'-'.time().'@'.$mailbox->getEmailDomain();
            $headers['Message-ID'] = $message_id;

            // If this is notification on message from customer, set customer as sender name
            $from_name = '';
            if ($last_thread->type == Thread::TYPE_CUSTOMER) {
                $from_name = '';
                if ($last_thread->customer) {
                    $from_name = $last_thread->customer->getFullName(true, true);
                }
                if ($from_name) {
                    $from_name = $from_name.' '.__('via').' '.$mailbox->name;
                }
            }
            if (!$from_name) {
                $from_name = $mailbox->name;
            }
            $from = ['address' => $mailbox->email, 'name' => $from_name];

            // Set user language
            app()->setLocale($user->getLocale());

            $headers['X-FreeScout-Mail-Type'] = 'user.notification';
            $headers = \Eventy::filter('jobs.send_reply_to_customer.headers', $headers, $user, $mailbox, $this->conversation, $this->threads, $from);

            $exception = null;

            try {
                Mail::to([['name' => $user->getFullName(), 'email' => $user->email]])
                    ->send(new UserNotification($user, $this->conversation, $this->threads, $headers, $from, $mailbox));
            } catch (\Exception $e) {
                // We come here if SMTP server unavailable for example
                activity()
                    ->causedBy($user)
                    ->withProperties([
                        'error'    => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
                     ])
                    ->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
                    ->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_USER);

                $exception = $e;
                $last_send_exception = $e;
            }

            $status_message = '';
            if ($exception) {
                $status = SendLog::STATUS_SEND_ERROR;
                $status_message = $exception->getMessage();
            } else {
                $failures = Mail::failures();

                // Save to send log
                if (!empty($failures) && in_array($user->email, $failures)) {
                    $status = SendLog::STATUS_SEND_ERROR;
                } else {
                    $status = SendLog::STATUS_ACCEPTED;
                }
            }

            SendLog::log($last_thread->id, $message_id, $user->email, SendLog::MAIL_TYPE_USER_NOTIFICATION, $status, null, $user->id, $status_message);
        }

        if ($last_send_exception) {
            // Retry job with delay.
            // https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
            // We do not try to resend Bounce messages: https://github.com/freescout-helpdesk/freescout/issues/3156
            if ($this->attempts() < $this->tries && !$last_thread->isBounce()) {
                if ($this->attempts() == 1) {
                    // Second attempt after 5 min.
                    $this->release(300);
                } else {
                    // Mail data has been passed to the mail server but after that
                    // we have not receved a response from the mail server (due to timeout).
                    // So we may consider that the email has been successfully sent.
                    // Otherwise this email may be sent again and again.
                    if ($this->attempts() >= 3
                        && preg_match("#^Connection to .* Timed Out$#", $last_send_exception->getMessage())
                        && \MailHelper::$smtp_data_sent
                    ) {
                        $this->fail($last_send_exception);
                    }

                    // Others - after 1 hour.
                    $this->release(3600);
                }

                throw $last_send_exception;
            } else {
                $this->fail($last_send_exception);

                return;
            }
        }
    }

    /**
     * The job failed to process.
     * This method is called after attempts had finished.
     * At this stage method has access only to variables passed in constructor.
     *
     * @param Exception $exception
     *
     * @return void
     */
    public function failed(\Exception $e)
    {
        // Write to activity log
        activity()
           //->causedBy($this->customer)
           ->withProperties([
                'error'    => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
            ])
           ->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
           ->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_USER);
    }
}
