<?php

/**
 * SendingServerElasticEmail class.
 *
 * Abstract class for Mailjet sending server
 *
 * LICENSE: This product includes software developed at
 * the Acelle Co., Ltd. (http://acellemail.com/).
 *
 * @category   MVC Model
 *
 * @author     N. Pham <n.pham@acellemail.com>
 * @author     L. Pham <l.pham@acellemail.com>
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace Acelle\Model;

use Acelle\Library\Log as MailLog;
use Acelle\Library\StringHelper;
use CurlFile;
use Acelle\Library\SendingServer\DomainVerificationInterface;

use function Acelle\Helpers\url_get_contents_ssl_safe;

class SendingServerElasticEmail extends SendingServer implements DomainVerificationInterface
{
    public const WEBHOOK = 'elasticemail';
    public const WEBHOOK_NAME = 'acellemail';
    public const API_ENDPOINT = 'https://api.elasticemail.com/v2';

    protected $table = 'sending_servers';
    public $client = null;
    public $client2 = null;
    public $isWebhookSetup = false;
    public $isCustomHeadersEnabled = false;

    public const DNS_SPF = 'v=spf1 a mx include:_spf.elasticemail.com ~all';

    /**
     * Get authenticated to Mailgun and return the session object.
     *
     * @return mixed
     */
    public function client()
    {
        if (!$this->client) {
            $this->client = new \ElasticEmail\ElasticEmail($this->api_key);
        }

        return $this->client;
    }

    // ELasticEmail Official SDK has a very strange PHP SDK
    // It works like this
    //
    //    $config = new Elastic\Config('API KEY');
    //    $instance = new CampaignInstance($config)   // or DomainInstance, or o
    public function client2($instance)
    {
        if (!$this->client2) {
            $this->client2 = new \ElasticEmail\ElasticEmail($this->api_key);
        }

        return $this->client;
    }


    /**
     * Process unsubscribe URL
     * See documentation at: https://elasticemail.com/support/user-interface/unsubscribe/#Unsubscribe-Link.
     *
     * @return string message
     */
    public function addUnsubscribeUrl($message)
    {
        return $message;
    }

    /**
     * Handle notification from ElasticEmail
     * Handle Bounce/Feedback/Error.
     *
     * @return mixed
     */
    public static function handleNotification($params)
    {
        // bounce
        if (strcasecmp($params['status'], 'Error') == 0) {
            $bounceLog = new BounceLog();

            // use Elastic Email transaction id as runtime-message-id
            $bounceLog->runtime_message_id = $params['transaction'];
            $trackingLog = TrackingLog::where('runtime_message_id', $bounceLog->runtime_message_id)->first();
            if ($trackingLog) {
                $bounceLog->message_id = $trackingLog->message_id;
            }

            $bounceLog->bounce_type = BounceLog::HARD;
            $bounceLog->raw = json_encode($params);
            $bounceLog->save();
            MailLog::info('Bounce recorded for message '.$bounceLog->runtime_message_id);

            // add subscriber's email to blacklist
            $subscriber = $bounceLog->findSubscriberByRuntimeMessageId();
            if ($subscriber) {
                $subscriber->sendToBlacklist($bounceLog->raw);
                MailLog::info('Email added to blacklist');
            } else {
                MailLog::warning('Cannot find associated tracking log for ElasticEmail message '.$bounceLog->runtime_message_id);
            }
        } elseif (strcasecmp($params['status'], 'AbuseReport') == 0) {
            $feedbackLog = new FeedbackLog();

            // use Elastic Email transaction id as runtime-message-id
            $feedbackLog->runtime_message_id = $params['transaction'];

            // retrieve the associated tracking log in Acelle
            $trackingLog = TrackingLog::where('runtime_message_id', $feedbackLog->runtime_message_id)->first();
            if ($trackingLog) {
                $feedbackLog->message_id = $trackingLog->message_id;
            }

            // ElasticEmail only notifies in case of SPAM reported
            $feedbackLog->feedback_type = 'spam';
            $feedbackLog->raw_feedback_content = json_encode($params);
            $feedbackLog->save();
            MailLog::info('Feedback recorded for message '.$feedbackLog->runtime_message_id);

            // update the mail list, subscriber to be marked as 'spam-reported'
            // @todo: the following lines of code should be wrapped up in one single method: $feedbackLog->markSubscriberAsSpamReported();
            $subscriber = $feedbackLog->findSubscriberByRuntimeMessageId();
            if ($subscriber) {
                $subscriber->markAsSpamReported();
                MailLog::info('Subscriber marked as spam-reported');
            } else {
                MailLog::warning('Cannot find associated tracking log for ElasticEmail message '.$feedbackLog->runtime_message_id);
            }
        }
    }

    /**
     * Enable custom headers.
     * By default, customers headers are suppressed by Elastic Email.
     *
     * @return mixed
     */
    public function enableCustomHeaders()
    {
        if ($this->isCustomHeadersEnabled) {
            return true;
        }

        $uri = self::API_ENDPOINT.'/account/updateadvancedoptions?apikey='.$this->api_key.'&allowCustomHeaders=true';

        $response = url_get_contents_ssl_safe($uri);
        $responseJson = json_decode($response);

        if ($responseJson->success == true) {
            MailLog::info('Custom headers enabled');
            $this->isCustomHeadersEnabled = true;
        } else {
            throw new \Exception('Cannot enable customer headers: '.$response);
        }
    }

    /**
     * Setup webhooks for processing bounce and feedback loop.
     *
     * @return mixed
     */
    public function setupWebhooks()
    {
        if ($this->isWebhookSetup) {
            return true;
        }

        // delete existing webhook
        $this->deleteWebHook();

        // add webhook
        $subscribeUrl = urlencode($this->getSubscribeUrl());
        $endpoint = self::API_ENDPOINT.'/account/addwebhook?apikey='.$this->api_key.'&webNotificationUrl='.$subscribeUrl.'&name='.self::WEBHOOK_NAME.'&notifyOncePerEmail=&notificationForSent=&notificationForOpened=&notificationForClicked=&notificationForUnsubscribed=&notificationForAbuseReport=true&notificationForError=true';

        $response = json_decode(url_get_contents_ssl_safe($endpoint), true);

        if ($response['success'] == true) {
            MailLog::info('webhook set!');
            $this->isWebhookSetup = true;
        } else {
            $msg = 'ElasticEmail Erorr. Cannot setup webhook for "'.$subscribeUrl.'". Make sure your Elastic API key has full access. Also, please note that the webhook feature is reserved for Elastic Unlimited PRO & API PRO plans only. Response from server: '.json_encode($response);
            MailLog::error($msg);
            MailLog::error('Elastic endpoint: '.$endpoint);
            throw new \Exception($msg);
        }
    }

    public function deleteWebHook()
    {
        MailLog::info('Deleting webhooks if any');
        $loadUri = self::API_ENDPOINT.'/account/loadwebhook?apikey='.$this->api_key.'&limit=1000';
        $webhooks = json_decode(url_get_contents_ssl_safe($loadUri), true)['data'];
        MailLog::info(sizeof($webhooks).' webhooks found');

        foreach ($webhooks as $webhook) {
            MailLog::info('Checking '.$webhook['url'].' ('.$webhook['name'].')');
            if ($this->isSubscribeUrl($webhook['url']) || $webhook['name'] == self::WEBHOOK_NAME) {
                MailLog::info('Deleting '.$webhook['url'].' ('.$webhook['name'].')');
                $deleteUri = $deleteUri = self::API_ENDPOINT.'/account/deletewebhook?apikey='.$this->api_key.'&webhookID='.$webhook['webhookid'];
                $result = json_decode(url_get_contents_ssl_safe($deleteUri), true);
                if ($result['success'] != true) {
                    MailLog::error('Cannot delete webhook '.$webhook['url'].' ('.$webhook['name'].')');
                    throw new \Exception('Cannot delete webhook '.$webhook['url'].' ('.$webhook['name'].')');
                }
            } else {
                MailLog::info('Skipped');
            }
        }
    }

    public function getSubscribeUrl()
    {
        return StringHelper::joinUrl(Setting::get('url_delivery_handler'), self::WEBHOOK);
    }

    private function isSubscribeUrl($url)
    {
        // simpley compare the provided url to see if it is the same as the subscribe one
        return join_paths($this->getSubscribeUrl(), '/') == join_paths($url, '/');
    }

    /**
     * Unescape the HTML attributes escaped by DOMDocument (inline CSS maker)
     * For example: conver <a href="%7Bunsubscribe:...%7D" to <a href="{unsubscribe:...}".
     *
     * @return mixed
     */
    public function unescapeUnsubscribeUrl($message)
    {
        preg_match_all('/(?<matched>\%7Bunsubscribe:.*?\%7D)/', $message, $result);
        foreach ($result['matched'] as $occurrence) {
            $message = str_replace($occurrence, urldecode($occurrence), $message);
        }

        return $message;
    }

    /**
     * Delivery message.
     *
     * @return mixed
     */
    public function sendElasticEmailV2($message)
    {
        // @todo: what if there are more than 2 parts?
        $html = null;
        $plain = null;

        foreach ($message->getChildren() as $part) {
            $contentType = $part->getContentType();

            if ($contentType == 'text/html') {
                $html = $part->getBody();
            } elseif ($contentType == 'text/plain') {
                $plain = $part->getBody();
            }
        }

        if (!is_null($html)) {
            $html = $this->unescapeUnsubscribeUrl($html);
        }

        // @todo: custom headers not correctly supported by Elastic Email API v2
        $fromEmail = array_keys($message->getFrom())[0];
        $fromName = (is_null($message->getFrom())) ? null : array_values($message->getFrom())[0];
        $toEmail = array_keys($message->getTo())[0];
        $replyToEmail = (is_null($message->getReplyTo())) ? null : array_keys($message->getReplyTo())[0];

        $params = [
            'to' => $toEmail,
            'replyTo' => $replyToEmail,
            'subject' => $message->getSubject(),
            'from' => $fromEmail,
            'fromName' => $fromName,
            'bodyHtml' => $html,
            'bodyText' => $plain,
            'charset' => 'utf-8',
        ];

        $attachmentKey = 1;
        foreach ($message->extAttachments as $file) {
            $params['file_'.$attachmentKey] = new CurlFile($file['path'], $file['type'], basename($file['path']));
            $attachmentKey += 1;
        }

        $result = $this->client()->email()->send($params);
        $jsonResponse = json_decode($result->getData());

        // Use transactionid returned from ElasticEmail as runtime_message_id
        return $jsonResponse->data->transactionid;
    }

    /**
     * Delivery message.
     *
     * @return mixed
     */
    public function sendElasticEmailV2Fixed($message)
    {
        // @todo: what if there are more than 2 parts?
        $html = null;
        $plain = null;

        foreach ($message->getChildren() as $part) {
            $contentType = $part->getContentType();

            if ($contentType == 'text/html') {
                $html = $part->getBody();
            } elseif ($contentType == 'text/plain') {
                $plain = $part->getBody();
            }
        }

        if (!is_null($html)) {
            $html = $this->unescapeUnsubscribeUrl($html);
        }

        // @todo: custom headers not correctly supported by Elastic Email API v2
        $fromEmail = array_keys($message->getFrom())[0];
        $fromName = (is_null($message->getFrom())) ? null : array_values($message->getFrom())[0];
        $toEmail = array_keys($message->getTo())[0];
        $replyToEmail = (is_null($message->getReplyTo())) ? null : array_keys($message->getReplyTo())[0];

        $params = [
            'apikey' => $this->api_key,
            'to' => $toEmail,
            'replyTo' => $replyToEmail,
            'subject' => $message->getSubject(),
            'from' => $fromEmail,
            'fromName' => $fromName,
            'bodyHtml' => $html,
            'bodyText' => $plain,
            'charset' => 'utf-8',
        ];

        $attachmentKey = 1;
        foreach ($message->extAttachments as $file) {
            $params['file_'.$attachmentKey] = new CurlFile($file['path'], $file['type'], basename($file['path']));
            $attachmentKey += 1;
        }

        $ch = curl_init();

        curl_setopt_array($ch, array(
            CURLOPT_URL => 'https://api.elasticemail.com/v2/email/send',
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $params,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_SSL_VERIFYPEER => false
        ));

        $result = curl_exec($ch);
        curl_close($ch);

        if ($result === false || empty($result)) {
            throw new \Exception("Cannot send using Elastic API, curl_exec returns false or empty");
        }

        $jsonResponse = json_decode($result);

        try {
            if ($jsonResponse->success == false) {
                throw new \Exception("ElasticEmail error: ".$result);
            }
        } catch (\Throwable $t) {
            throw new \Exception("Abnormal response from ElasticEmail: ".$result);
        }

        // Use transactionid returned from ElasticEmail as runtime_message_id
        return $jsonResponse->data->transactionid;
    }

    /**
     * Get verified domains.
     *
     * @return array
     */
    public function syncIdentities()
    {
        // Sample result:
        // {"success":true,"data":[{"domain":"marcurselli.com (info@marcurselli.com)","defaultdomain":true,"spf":true,"dkim":true,"mx":true,"dmarc":true,"isrewritedomainvalid":true,"verify":false,"type":0,"trackingstatus":0,"certificatestatus":0,"certificatevalidationerror":null,"trackingtypeuserrequest":null,"verp":true,"custombouncesdomain":"bounces.marcurselli.com","iscustombouncesdomaindefault":true,"ownership":0},{"domain":"acellemail.com","defaultdomain":false,"spf":false,"dkim":false,"mx":true,"dmarc":false,"isrewritedomainvalid":false,"verify":true,"type":0,"trackingstatus":2,"certificatestatus":0,"certificatevalidationerror":null,"trackingtypeuserrequest":null,"verp":true,"custombouncesdomain":null,"iscustombouncesdomaindefault":false,"ownership":0}]}
        $response = url_get_contents_ssl_safe(self::API_ENDPOINT.'/domain/list?apikey='.$this->api_key);
        $json = json_decode($response, true);
        if ($json['success'] == false) {
            throw new \Exception($json['error']);
        }

        // just in case the 'domain' value comes in "domain.com (admin@emailaddress.com)"
        $identities = [];
        foreach ($json['data'] as $domain) {
            $name = preg_split('/\s+/', $domain['domain'])[0];

            // Notice that verify:true means NOT verified while verify:false => already verified!!!! STUPID!
            // Use DKIM instead
            $identities[$name] = ['VerificationStatus' => ($domain['dkim'] == true)];
        }

        $addedByUsers = $this->sendingDomains()->whereIn('name', array_keys($identities))->get();

        foreach ($addedByUsers as $domain) {
            if (array_key_exists($domain->name, $identities)) {
                $identities[$domain->name]['UserId'] = $domain->customer->id;
                $identities[$domain->name]['UserName'] = $domain->customer->displayName();
            }
        }

        $identityStore = $this->getIdentityStore();
        $identityStore->update($identities);

        $options = $this->getOptions();
        $options['identities'] = $identityStore->get();
        $this->setOptions($options);
        $this->save();
    }

    /**
     * Check the sending server settings, make sure it does work.
     *
     * @return bool
     */
    public function test()
    {
        $response = url_get_contents_ssl_safe(self::API_ENDPOINT.'/domain/list?apikey='.$this->api_key);
        $json = json_decode($response, true);
        if ($json['success'] == false) {
            throw new \Exception('Failed to connect to ElasticEmail: '.$response);
        }

        return true;
    }

    /**
     * Allow user to verify his/her own sending domain against Acelle Mail.
     *
     * @return bool
     */
    public function allowVerifyingOwnDomains()
    {
        return false;
    }

    /**
     * Allow user to verify his/her own sending domain against Acelle Mail.
     *
     * @return bool
     */
    public function allowVerifyingOwnEmails()
    {
        return false;
    }

    /**
     * Allow user to verify his/her own emails against AWS.
     *
     * @return bool
     */
    public function allowVerifyingOwnDomainsRemotely()
    {
        return true;
    }

    /**
     * Allow user to verify his/her own emails against AWS.
     *
     * @return bool
     */
    public function allowVerifyingOwnEmailsRemotely()
    {
        return false;
    }

    public function setupBeforeSend($fromEmailAddress)
    {
        try {
            $this->setupWebhooks();
        } catch (\Exception $ex) {
            MailLog::warning('Cannot set up Elastic webhook: ' . $ex->getMessage());
        }
    }

    private function curlExecute($method, $action, $params = [])
    {
        $url = "https://api.elasticemail.com/v4/{$action}/";

        if ($method == 'GET') {
            $url = "$url?".http_build_query($params);

            $ch = curl_init($url);
        } else {
            $ch = curl_init($url);
            curl_setopt_array($ch, array(
                CURLOPT_CUSTOMREQUEST => $method,
                CURLOPT_POSTFIELDS => json_encode($params),
                CURLOPT_HEADER => false,
                CURLOPT_SSL_VERIFYPEER => false,
            ));
        }

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            "X-ElasticEmail-ApiKey: {$this->api_key}"
        ]);

        $result = curl_exec($ch);

        // Handle errors
        if (curl_errno($ch)) {
            throw new \Exception("Cannot execute '{$action}' using Elastic API, curl error: ".curl_error($ch));
        }

        if ($result === false || empty($result)) {
            throw new \Exception("Cannot execute '{$action}' using Elastic API, curl_exec returns false or empty");
        }

        curl_close($ch);

        $jsonResponse = json_decode($result);

        if (!empty($jsonResponse->Error)) {
            throw new \Exception("ElasticEmail error: ".$result);
        } else {
            return $jsonResponse;
        }
    }

    public function getDomainInfo($domain)
    {
        // @important: do not use the GET /domains/$name (individual domain name) API
        // as it returns ERROR in case the speicified domain does not exist
        // it messes up with the case when there is another error with the request (invalid key, timeout...)

        // So, retrive the list of domains and check one by one

        $existed = false;
        foreach ($this->getAllDomains() as $r) {
            if ($r->Domain == $domain) {
                return $r;
            }
        }

        return null;
    }

    public function getAllDomains()
    {
        $action = "domains";
        $allDomains = $this->curlExecute('GET', $action);
        return $allDomains;
    }

    public function addDomain(string $domain): array
    {
        if ($this->getDomainInfo($domain)) {
            // Domain already exists
        } else {
            $params = [
                'domain' => $domain,
            ];

            $result = $this->curlExecute('POST', $action = 'domain', $params);
        }

        // ElasticEmail API response does not include DNS info, resorted to get those from dashboard and hard-code them here
        return [
            'identity' => [
                'name' => 'tracking',
                'type' => 'CNAME',
                'value' => 'api.elasticemail.com',
            ],

            'dmarc' => [ 'name' => '_dmarc', 'type' => 'TXT', 'value' => 'v=DMARC1;p=none;' ],

            'dkim' => [
                [ 'name' => 'api._domainkey' ], // Convention here: 'value' missing/empty means not available in the web UI
            ],

            'spf' => [
                [
                    'name' => '@',
                    'value' => 'v=spf1 a mx include:_spf.elasticemail.com ~all',
                    'type' => 'TXT'
                ]
            ],

            'results' => [
                'identity' => false,
                'dkim' => false,
                'spf' => false,
                'dmarc' => false,
            ],
        ];
    }

    // Interface 2/2: check verification status
    public function checkDomainVerificationStatus(SendingDomain $domain): array
    {
        $action = "domains/{$domain->name}/verification";
        $info = $this->curlExecute('PUT', $action, "None");

        $identity = !$info->Verify; // TRUE does not mean OK, instead, it means there are more to verify (i.e. not fully verified)
        $dkim = $info->Dkim; // DKIM is considered done if DMARC is also done
        $spf = $info->Spf;
        $dmarc = $info->DMARC;
        $finalStatus = $identity && $dkim && $spf;

        // Notice: in the API response, there is no separate verification result for 'tracking CNAME api.elasticemail.com'
        // As a result, even if the DNS is set, there is no way to know, so, we simply show 'pending' to users
        // Don't worry, it will turn to 'verified' if all DKIM, SPF and DMARC are all verified

        return [ $identity, $dkim, $spf, $dmarc, $finalStatus ];
    }

    public function getServiceName(): string
    {
        return "ElasticEmail";
    }
}
