<?php defined('MW_PATH') || exit('No direct script access allowed');

/**
 * CampaignSenderBehavior
 * 
 * @package MailWizz EMA
 * @author Serban George Cristian <cristian.serban@mailwizz.com> 
 * @link http://www.mailwizz.com/
 * @copyright 2013-2014 MailWizz EMA (http://www.mailwizz.com)
 * @license http://www.mailwizz.com/license/
 * @since 1.0
 */
 
class CampaignSenderBehavior extends CBehavior
{
    public function sendCampaign()
    {
        $campaign = $this->getOwner();
        if (!in_array($campaign->status, array(Campaign::STATUS_SENDING, Campaign::STATUS_PENDING_SENDING))) {
            return;
        }
        
        // in case error or uncought exception
        $this->attachSafetyHandlers();
        
        $server = DeliveryServer::pickServer(0, $campaign);
        if (empty($server)) {
            $this->detachSafetyHandlers();
            return;
        }

        // first thing first is to mark this campaign as processing.
        $campaign->status = Campaign::STATUS_PROCESSING;
        $campaign->save(false);
        
        // find the subscribers we need to send these emails at
        $subscribers = $this->findSubscribers();

        // it's done.
        if (empty($subscribers)) {
            $this->markCampaignSent();
            return;
        }

        try {
            
            $options = Yii::app()->options;
            $list = $campaign->list;
            
            $mailerPlugins = array(
                'logger' => true,
            );
            
            if ($sendAtOnce = (int)$options->get('system.cron.send_campaigns.send_at_once', 100)) {
                $mailerPlugins['antiFloodPlugin'] = array(
                    'sendAtOnce'    => $sendAtOnce,
                    'pause'         => (int)$options->get('system.cron.send_campaigns.pause', 30),
                );
            }
            
            if ($perMinute = (int)$options->get('system.cron.send_campaigns.emails_per_minute', 100)) {
                $mailerPlugins['throttlePlugin'] = array(
                    'perMinute' => $perMinute,
                );
            }
            
            $attachments = CampaignAttachment::model()->findAll(array(
                'select'    => 'file',
                'condition' => 'campaign_id = :cid',
                'params'    => array(':cid' => $campaign->campaign_id),
            ));
            
            $processedCounter = 0;
            $serverHasChanged = false;
            $changeServerAt   = (int)$options->get('system.cron.send_campaigns.change_server_at', 100);
            
            foreach ($subscribers as $subscriber) {
                
                $_campaign = Campaign::model()->find(array(
                    'select'    => 'status',
                    'condition' => 'campaign_id = :cid',
                    'params'    => array(':cid' => (int)$campaign->campaign_id), 
                ));
                
                if (!$_campaign->isProcessing) {
                    throw new Exception(Yii::t('campaigns', 'Campaign state has been changed by external command!'));
                }
                
                if ($server->getIsOverQuota()) {
                    $currentServerId = $server->server_id;
                    if (!($server = DeliveryServer::pickServer($currentServerId, $campaign))) {
                        throw new Exception(Yii::t('campaigns', 'Cannot find a valid server to send the campaign email, aborting until a delivery server is available!'));
                    }
                }

                // if blacklisted, goodbye.
                if ($subscriber->getIsBlacklisted()) {
                    $this->logDelivery($subscriber, Yii::t('campaigns', 'This email is blacklisted. Sending is denied!'), CampaignDeliveryLog::STATUS_BLACKLISTED);
                    continue;
                }
                
                $emailParams = $this->prepareEmail($subscriber);
                if (empty($emailParams) || !is_array($emailParams)) {
                    $this->logDelivery($subscriber, Yii::t('campaigns', 'Unable to prepare the email content!'), CampaignDeliveryLog::STATUS_ERROR);
                    continue;
                }
                
                if ($changeServerAt > 0 && $processedCounter >= $changeServerAt && !$serverHasChanged) {
                    $currentServerId = $server->server_id;
                    if ($newServer = DeliveryServer::pickServer($currentServerId, $campaign)) {
                        $server = $newServer;
                        unset($newServer);
                    }
                    
                    $processedCounter = 0;
                    $serverHasChanged = true;
                }
                
                // $emailParams = $server->getParamsArray($emailParams);
                $emailParams['from']    = array($server->getFromEmail() => $campaign->from_name);
                $emailParams['replyTo'] = array($campaign->reply_to => $campaign->from_name);

                $listUnsubscribeHeaderValue = $options->get('system.urls.frontend_absolute_url');
                $listUnsubscribeHeaderValue .= 'lists/'.$list->list_uid.'/unsubscribe/'.$subscriber->subscriber_uid . '/' . $campaign->campaign_uid;
                $listUnsubscribeHeaderValue = '<'.$listUnsubscribeHeaderValue.'>';
                
                $emailParams['headers'] = array(
                    'X-MW-CAMPAIGN-UID'     => $campaign->campaign_uid,
                    'X-MW-SUBSCRIBER-UID'   => $subscriber->subscriber_uid, 
                    'List-Unsubscribe'      => $listUnsubscribeHeaderValue
                );
                
                $emailParams = CMap::mergeArray($emailParams, $mailerPlugins);

                if (!empty($attachments)) {
                    $emailParams['attachments'] = array();
                    foreach ($attachments as $attachment) {
                        $emailParams['attachments'][] = Yii::getPathOfAlias('root') . $attachment->file;
                    }
                }

                $sent     = false;
                $response = null;
                $status   = CampaignDeliveryLog::STATUS_ERROR;
                
                $processedCounter++;
                if ($processedCounter >= $changeServerAt) {
                    $serverHasChanged = false;
                }
                       
                for ($i = 0; $i < 1; ++$i) {
                    
                    if ($logger = $server->getMailer()->getLoggerPlugin()) {
                        $logger->clear();
                    }

                    if ($sent = $server->setDeliveryFor(DeliveryServer::DELIVERY_FOR_CAMPAIGN)->setDeliveryObject($campaign)->sendEmail($emailParams)) {
                        $status = CampaignDeliveryLog::STATUS_SUCCESS;
                        if ($logger) {
                            $response = $logger->dump();
                        }
                        break;
                    }
                    
                    if ($logger) {
                        $response = $logger->dump();
                    }
                    
                    // if this is a fatal error, give up.
                    // http://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml
                    if(preg_match('/!!.*but\sgot\scode\s"(\d+)"/ix', $response, $matches)) {
                        if ((int)$matches[1] >= 450) {
                            $status = CampaignDeliveryLog::STATUS_FATAL_ERROR;
                            break;
                        } 
                    }
                    
                    $currentServerId = $server->server_id;
                    if ($newServer = DeliveryServer::pickServer($currentServerId, $campaign)) {
                        $server = $newServer;
                        unset($newServer);
                    }
                    
                    // this must be out of the if statement, it's no mistake :)
                    $serverHasChanged = true;
                    
                    // this means there's only one server for sending.
                    if ($server->server_id == $currentServerId) {
                        $changeServerAt = 0;
                        break;
                    }
                }
                
                // save the log now.
                $this->logDelivery($subscriber, $response, $status);
            }
            
        } catch (Exception $e) {
            // something went terribly wrong, do something with $e->getMessage();
            Yii::log($e->getMessage(), CLogger::LEVEL_ERROR);
            
            // respawn campaign
            $campaign->status = Campaign::STATUS_SENDING;
            $campaign->save(false);
            $this->detachSafetyHandlers();
            return;
        }
        
        // the sending batch is over. if we don't have enough subscribers for next batch, we stop.
        $subscribers = $this->countSubscribers();
        if (empty($subscribers)) {
            $this->markCampaignSent();
            return;
        }
        
        // allot of processing, ending with changing back the status
        $campaign->status = Campaign::STATUS_SENDING;
        $campaign->save(false);
        
        $this->detachSafetyHandlers();
    }
    
    protected function logDelivery(ListSubscriber $subscriber, $message, $status)
    {
        $campaign = $this->getOwner();
        
        $deliveryLog = new CampaignDeliveryLog();
        $deliveryLog->campaign_id = $campaign->campaign_id;
        $deliveryLog->subscriber_id = $subscriber->subscriber_id;
        $deliveryLog->message = str_replace("\n\n", "\n", $message);
        $deliveryLog->status = $status;
        
        return $deliveryLog->save();
    }
    
    protected function countSubscribers()
    {
        $campaign = $this->getOwner();
        if (!empty($campaign->segment_id)) {
            $count = $this->countSubscribersBySegment();
        } else {
            $count = $this->countSubscribersByList();
        }

        return $count;
    }
    
    protected function countSubscribersBySegment()
    {
        $campaign = $this->getOwner();
        $segment = $campaign->segment;
        
        // do not include people we already sent emails to.
        $criteria = new CDbCriteria();
        $criteria->select = 't.subscriber_id, t.subscriber_uid, t.email';
        $criteria->compare('list_id', $campaign->list_id);
        $criteria->compare('t.status', ListSubscriber::STATUS_CONFIRMED);
        $criteria->with = array(
            'deliveryLogs' => array(
                'select'    => false,
                'together'  => true,
                'joinType'  => 'LEFT OUTER JOIN',
                'on'        => 'deliveryLogs.campaign_id = :cid',
                'condition' => 'deliveryLogs.subscriber_id IS NULL',
                'params'    => array(':cid' => $campaign->campaign_id),
            ),
        );
        
        return $segment->countSubscribers($criteria);
    }
    
    protected function countSubscribersByList()
    {
        $campaign = $this->getOwner();

        $criteria = new CDbCriteria();
        $criteria->select = 't.subscriber_id, t.subscriber_uid, t.email';
        $criteria->compare('t.list_id', $campaign->list_id);
        $criteria->compare('t.status', ListSubscriber::STATUS_CONFIRMED);
        $criteria->with = array(
            'deliveryLogs' => array(
                'select'    => false,
                'together'  => true,
                'joinType'  => 'LEFT OUTER JOIN',
                'on'        => 'deliveryLogs.campaign_id = :cid',
                'condition' => 'deliveryLogs.subscriber_id IS NULL',
                'params'    => array(':cid' => $campaign->campaign_id),
            ),
        );

        return ListSubscriber::model()->count($criteria);
    }
    
    // find and sort subscribers
    protected function findSubscribers()
    {
        $campaign = $this->getOwner();
        if (!empty($campaign->segment_id)) {
            $subscribers = $this->findSubscribersBySegment();
        } else {
            $subscribers = $this->findSubscribersByList();
        }
        
        if (!empty($subscribers)) {
            $subscribers = $this->sortSubscribers($subscribers);    
        }
        
        return $subscribers;
    }
    
    protected function findSubscribersBySegment()
    {
        $campaign = $this->getOwner();
        $segment = $campaign->segment;
        
        // do not include people we already sent emails to.
        $criteria = new CDbCriteria();
        $criteria->select = 't.subscriber_id, t.subscriber_uid, t.email';
        $criteria->compare('t.list_id', $campaign->list_id);
        $criteria->compare('t.status', ListSubscriber::STATUS_CONFIRMED);
        $criteria->with = array(
            'deliveryLogs' => array(
                'select'        => false,
                'together'      => true,
                'joinType'      => 'LEFT OUTER JOIN',
                'on'            => 'deliveryLogs.campaign_id = :cid',
                'condition'     => 'deliveryLogs.subscriber_id IS NULL',
                'params'        => array(':cid' => $campaign->campaign_id),
            ),
        );
        $limit = (int)Yii::app()->options->get('system.cron.send_campaigns.subscribers_at_once', 300);
        return $segment->findSubscribers(0, $limit, $criteria);
    }
    
    protected function findSubscribersByList()
    {
        $campaign = $this->getOwner();

        $criteria = new CDbCriteria();
        $criteria->select = 't.subscriber_id, t.subscriber_uid, t.email';
        $criteria->compare('t.list_id', $campaign->list_id);
        $criteria->compare('t.status', ListSubscriber::STATUS_CONFIRMED);
        $criteria->with = array(
            'deliveryLogs' => array(
                'select'        => false,
                'together'      => true,
                'joinType'      => 'LEFT OUTER JOIN',
                'on'            => 'deliveryLogs.campaign_id = :cid',
                'condition'     => 'deliveryLogs.subscriber_id IS NULL',
                'params'        => array(':cid' => $campaign->campaign_id),
            ),
        );
        
        $limit = (int)Yii::app()->options->get('system.cron.send_campaigns.subscribers_at_once', 300);
        
        $criteria->offset = 0;
        $criteria->limit = $limit;
        
        return ListSubscriber::model()->findAll($criteria);
    }
    
    /**
     * Tries to: 
     * 1. Group the subscribers by domain
     * 2. Sort them so that we don't send to same domain two times in a row. 
     */
    protected function sortSubscribers($subscribers)
    {
        $subscribersCount = count($subscribers);
        $_subscribers = array();
        foreach ($subscribers as $index => $subscriber) {
            $emailParts = explode('@', $subscriber->email);
            $domainName = $emailParts[1];
            if (!isset($_subscribers[$domainName])) {
                $_subscribers[$domainName] = array();
            }
            $_subscribers[$domainName][] = $subscriber;
            unset($subscribers[$index]);
        }
        
        $subscribers = array();
        while ($subscribersCount > 0) {
            foreach ($_subscribers as $domainName => $subs) {
                foreach ($subs as $index => $sub) {
                    $subscribers[] = $sub;
                    unset($_subscribers[$domainName][$index]);
                    break;
                }
            }
            $subscribersCount--;
        }
        
        return $subscribers;
    }
    
    protected function prepareEmail($subscriber)
    {
        $campaign = $this->getOwner();
        
        // how come ?
        if (empty($campaign->template)) {
            return false;
        }
        
        $emailContent = $campaign->template->content;
        
        if (!empty($campaign->option) && $campaign->option->xml_feed == CampaignOption::TEXT_YES) {
            $emailContent = CampaignXmlFeedParser::parseContent($emailContent, $campaign, true);
        }
        
        if (!empty($campaign->option) && $campaign->option->json_feed == CampaignOption::TEXT_YES) {
            $emailContent = CampaignJsonFeedParser::parseContent($emailContent, $campaign, true);
        }
        
        if (!empty($campaign->option) && $campaign->option->url_tracking == CampaignOption::TEXT_YES) {
            $emailContent = CampaignHelper::transformLinksForTracking($emailContent, $campaign, $subscriber, true);
        }
        
        $emailData = CampaignHelper::parseContent($emailContent, $campaign, $subscriber, true);
        
        list($toName, $emailSubject, $emailContent) = $emailData;
        
        $emailAddress = $subscriber->email;
        
        // Plain TEXT only supports basic tags transform, no xml/json feeds nor tracking.
        $emailPlainText = null;
        if (!empty($campaign->option) && $campaign->option->plain_text_email == CampaignOption::TEXT_YES) {
            if ($campaign->template->auto_plain_text === CampaignTemplate::TEXT_YES && empty($campaign->template->plain_text)) {
                $emailPlainText = CampaignHelper::htmlToText($emailContent);
            }
            
            if (empty($emailPlainText) && !empty($campaign->template->plain_text)) {
                $_emailData = CampaignHelper::parseContent($campaign->template->plain_text, $campaign, $subscriber, false);
                list(, , $emailPlainText) = $_emailData;
            }
        }
        
        return array(
            'to'        => array($emailAddress => $toName),
            'subject'   => $emailSubject,
            'body'      => $emailContent,
            'plainText' => $emailPlainText,
        );
    }
    
    protected function markCampaignSent()
    {
        $campaign = $this->getOwner();
        $campaign->status = Campaign::STATUS_SENT;
        $campaign->save(false);
            
        if (Yii::app()->options->get('system.customer.action_logging_enabled', true)) {
            $list = $campaign->list;
            $customer = $list->customer;
            if (!($logAction = $customer->asa('logAction'))) {
                $customer->attachBehavior('logAction', array(
                    'class' => 'customer.components.behaviors.CustomerActionLogBehavior',
                ));
                $logAction = $customer->asa('logAction');        
            }
            $logAction->campaignSent($campaign);
        }
        
        $this->detachSafetyHandlers();
        
        $this->sendCampaignStats();
    }
    
    protected function sendCampaignStats()
    {
        $campaign = $this->getOwner();
        if (empty($campaign->option->email_stats) || !filter_var($campaign->option->email_stats, FILTER_VALIDATE_EMAIL)) {
            return;
        }
        
        if (!($server = DeliveryServer::pickServer())) {
            return;
        }
        
        $campaign->attachBehavior('stats', array(
            'class' => 'customer.components.behaviors.CampaignStatsProcessorBehavior',
        ));
        $viewData   = compact('campaign');
        
        // prepare and send the email.
        $emailTemplate  = Yii::app()->options->get('system.email_templates.common');
        $emailBody      = Yii::app()->command->renderFile(Yii::getPathOfAlias('console.views.campaign-stats').'.php', $viewData, true);
        $emailTemplate  = str_replace('[CONTENT]', $emailBody, $emailTemplate);

        $emailParams            = array();
        $emailParams['from']    = array($server->getFromEmail() => $campaign->from_name);
        $emailParams['replyTo'] = array($campaign->reply_to => $campaign->from_name);
        $emailParams['to']      = array($campaign->option->email_stats => $campaign->from_name);
        $emailParams['subject'] = Yii::t('campaign_reports', 'The campaign {name} has finished sending, here are the stats', array('{name}' => $campaign->name));
        $emailParams['body']    = $emailTemplate;
        
        return $server->setDeliveryFor(DeliveryServer::DELIVERY_FOR_CAMPAIGN)->setDeliveryObject($campaign)->sendEmail($emailParams);
    }
    
    protected function attachSafetyHandlers()
    {
        Yii::app()->attachEventHandler('onError', array($this, '_restoreCampaignStatus'));
        Yii::app()->attachEventHandler('onException', array($this, '_restoreCampaignStatus'));
    }
    
    protected function detachSafetyHandlers()
    {
        Yii::app()->detachEventHandler('onError', array($this, '_restoreCampaignStatus'));
        Yii::app()->detachEventHandler('onException', array($this, '_restoreCampaignStatus'));
    }
    
    public function _restoreCampaignStatus($event)
    {
        $campaign = $this->getOwner();
        $campaign->status = Campaign::STATUS_SENDING;
        $campaign->save(false);
    }
}