<?php
namespace Grav\Plugin\Email;

use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Twig\Twig;
use Grav\Framework\Form\Interfaces\FormInterface;
use \Monolog\Logger;
use \Monolog\Handler\StreamHandler;

class Email
{
    /**
     * @var \Swift_Transport
     */
    protected $mailer;

    /**
     * @var \Swift_Plugins_LoggerPlugin
     */
    protected $logger;

    protected $queue_path;

    /**
     * Returns true if emails have been enabled in the system.
     *
     * @return bool
     */
    public static function enabled()
    {
        return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none';
    }

    /**
     * Returns true if debugging on emails has been enabled.
     *
     * @return bool
     */
    public static function debug()
    {
        return Grav::instance()['config']->get('plugins.email.debug') == 'true';
    }

    /**
     * Creates an email message.
     *
     * @param string $subject
     * @param string $body
     * @param string $contentType
     * @param string $charset
     * @return \Swift_Message
     */
    public function message($subject = null, $body = null, $contentType = null, $charset = null)
    {
        return new \Swift_Message($subject, $body, $contentType, $charset);
    }

    /**
     * Creates an attachment.
     *
     * @param string $data
     * @param string $filename
     * @param string $contentType
     * @return \Swift_Attachment
     */
    public function attachment($data = null, $filename = null, $contentType = null)
    {
        return new \Swift_Attachment($data, $filename, $contentType);
    }

    /**
     * Creates an embedded attachment.
     *
     * @param string $data
     * @param string $filename
     * @param string $contentType
     * @return \Swift_EmbeddedFile
     */
    public function embedded($data = null, $filename = null, $contentType = null)
    {
        return new \Swift_EmbeddedFile($data, $filename, $contentType);
    }

    /**
     * Creates an image attachment.
     *
     * @param string $data
     * @param string $filename
     * @param string $contentType
     * @return \Swift_Image
     */
    public function image($data = null, $filename = null, $contentType = null)
    {
        return new \Swift_Image($data, $filename, $contentType);
    }

    /**
     * Send email.
     *
     * @param \Swift_Message $message
     * @return int
     */
    public function send($message)
    {
        $mailer = $this->getMailer();

        $result = $mailer ? $mailer->send($message) : 0;

        // Check if emails and debugging are both enabled.
        if ($mailer && $this->debug()) {

            $log = new Logger('email');
            $locator = Grav::instance()['locator'];
            $log_file = $locator->findResource('log://email.log', true, true);
            $log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));

            // Append the SwiftMailer log to the log.
            $log->addDebug($this->getLogs());
        }

        return $result;
    }

    /**
     * Build e-mail message.
     *
     * @param array $params
     * @param array $vars
     * @return \Swift_Message
     */
    public function buildMessage(array $params, array $vars = [])
    {
        /** @var Twig $twig */
        $twig = Grav::instance()['twig'];

        /** @var Config $config */
        $config = Grav::instance()['config'];

        /** @var Language $language */
        $language = Grav::instance()['language'];

        // Extend parameters with defaults.
        $params += [
            'bcc' => $config->get('plugins.email.bcc', []),
            'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'),
            'cc' => $config->get('plugins.email.cc', []),
            'cc_name' => $config->get('plugins.email.cc_name'),
            'charset' =>  $config->get('plugins.email.charset', 'utf-8'),
            'from' => $config->get('plugins.email.from'),
            'from_name' => $config->get('plugins.email.from_name'),
            'content_type' => $config->get('plugins.email.content_type', 'text/html'),
            'reply_to' => $config->get('plugins.email.reply_to', []),
            'reply_to_name' => $config->get('plugins.email.reply_to_name'),
            'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null,
            'to' => $config->get('plugins.email.to'),
            'to_name' => $config->get('plugins.email.to_name'),
            'process_markdown' => false,
            'template' => false
        ];

        // Create message object.
        $message = $this->message();

        if (!$params['to']) {
            throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS'));
        }
        if (!$params['from']) {
            throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS'));
        }

        // make email configuration available to templates
        $vars += [
            'email' => $params,
        ];

        // Process parameters.
        foreach ($params as $key => $value) {
            switch ($key) {
                case 'body':
                    if (is_string($value)) {
                        $body = $twig->processString($value, $vars);

                        if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
                            $parsedown = new Parsedown();
                            $body = $parsedown->text($body);
                        }

                        if ($params['template']) {
                            $vars = array_merge($vars, ['content' => $body]);
                            $body = $twig->processTemplate($params['template'], $vars);
                        }

                        $content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null;
                        $charset = !empty($params['charset']) ? $twig->processString($params['charset'], $vars) : null;

                        $message->setBody($body, $content_type, $charset);
                    }
                    elseif (is_array($value)) {
                        foreach ($value as $body_part) {
                            $body_part += [
                                'charset' => $params['charset'],
                                'content_type' => $params['content_type'],
                            ];

                            $body = !empty($body_part['body']) ? $twig->processString($body_part['body'], $vars) : null;

                            if ($params['process_markdown'] && $body_part['content_type'] === 'text/html') {
                                $parsedown = new Parsedown();
                                $body = $parsedown->text($body);
                            }

                            if (isset($body_part['template'])) {
                                $vars = array_merge($vars, ['content' => $body]);
                                $body = $twig->processTemplate($body_part['template'], $vars);
                            }

                            $content_type = !empty($body_part['content_type']) ? $twig->processString($body_part['content_type'], $vars) : null;
                            $charset = !empty($body_part['charset']) ? $twig->processString($body_part['charset'], $vars) : null;

                            if (!$message->getBody()) {
                                $message->setBody($body, $content_type, $charset);
                            }
                            else {
                                $message->addPart($body, $content_type, $charset);
                            }
                        }
                    }
                    break;

                case 'subject':
                    $message->setSubject($twig->processString($language->translate($value), $vars));
                    break;

                case 'to':
                    if (is_string($value) && !empty($params['to_name'])) {
                        $value = [
                            'mail' => $twig->processString($value, $vars),
                            'name' => $twig->processString($params['to_name'], $vars),
                        ];
                    }

                    foreach ($this->parseAddressValue($value, $vars) as $address) {
                        $message->addTo($address->mail, $address->name);
                    }
                    break;

                case 'cc':
                    if (is_string($value) && !empty($params['cc_name'])) {
                        $value = [
                            'mail' => $twig->processString($value, $vars),
                            'name' => $twig->processString($params['cc_name'], $vars),
                        ];
                    }

                    foreach ($this->parseAddressValue($value, $vars) as $address) {
                        $message->addCc($address->mail, $address->name);
                    }
                    break;

                case 'bcc':
                    foreach ($this->parseAddressValue($value, $vars) as $address) {
                        $message->addBcc($address->mail, $address->name);
                    }
                    break;

                case 'from':
                    if (is_string($value) && !empty($params['from_name'])) {
                        $value = [
                            'mail' => $twig->processString($value, $vars),
                            'name' => $twig->processString($params['from_name'], $vars),
                        ];
                    }

                    foreach ($this->parseAddressValue($value, $vars) as $address) {
                        $message->addFrom($address->mail, $address->name);
                    }
                    break;

                case 'reply_to':
                    if (is_string($value) && !empty($params['reply_to_name'])) {
                        $value = [
                            'mail' => $twig->processString($value, $vars),
                            'name' => $twig->processString($params['reply_to_name'], $vars),
                        ];
                    }

                    foreach ($this->parseAddressValue($value, $vars) as $address) {
                        $message->addReplyTo($address->mail, $address->name);
                    }
                    break;

            }
        }

        return $message;
    }

    /**
     * Return parsed e-mail address value.
     *
     * @param string|string[] $value
     * @param array $vars
     * @return array
     */
    public function parseAddressValue($value, array $vars = [])
    {
        $parsed = [];

        /** @var Twig $twig */
        $twig = Grav::instance()['twig'];

        // Single e-mail address string
        if (is_string($value)) {
            $parsed[] = (object) [
                'mail' => $twig->processString($value, $vars),
                'name' => null,
            ];
        }

        else {
            // Cast value as array
            $value = (array) $value;

            // Single e-mail address array
            if (!empty($value['mail'])) {
                $parsed[] = (object) [
                    'mail' => $twig->processString($value['mail'], $vars),
                    'name' => !empty($value['name']) ? $twig->processString($value['name'], $vars) : NULL,
                ];
            }

            // Multiple addresses (either as strings or arrays)
            elseif (!(empty($value['mail']) && !empty($value['name']))) {
                foreach ($value as $y => $itemx) {
                    $addresses = $this->parseAddressValue($itemx, $vars);

                    if (($address = reset($addresses))) {
                        $parsed[] = $address;
                    }
                }
            }
        }

        return $parsed;
    }

    /**
     * Return debugging logs if enabled
     *
     * @return string
     */
    public function getLogs()
    {
        if ($this->debug()) {
            return $this->logger->dump();
        }
        return '';
    }

    /**
     * @internal
     * @return null|\Swift_Mailer
     */
    protected function getMailer()
    {
        if (!$this->enabled()) {
            return null;
        }

        if (!$this->mailer) {
            /** @var Config $config */
            $config = Grav::instance()['config'];
            $queue_enabled = $config->get('plugins.email.queue.enabled');

            $transport = $queue_enabled === true ? $this->getQueue() : $this->getTransport();

            // Create the Mailer using your created Transport
            $this->mailer = new \Swift_Mailer($transport);

            // Register the logger if we're debugging.
            if ($this->debug()) {
                $this->logger = new \Swift_Plugins_Loggers_ArrayLogger();
                $this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($this->logger));
            }
        }

        return $this->mailer;
    }

    protected static function getQueuePath()
    {
        $queue_path = Grav::instance()['locator']->findResource('user://data', true) . '/email-queue';

        if (!file_exists($queue_path)) {
            mkdir($queue_path);
        }

        return $queue_path;
    }

    protected static function getQueue()
    {
        $queue_path = static::getQueuePath();

        $spool = new \Swift_FileSpool($queue_path);
        $transport = new \Swift_SpoolTransport($spool);

        return $transport;
    }

    public static function flushQueue()
    {
        $grav = Grav::instance();

        $grav['debugger']->enabled(false);

        $config = $grav['config']->get('plugins.email.queue');

        try {
            $queue = static::getQueue();
            $spool = $queue->getSpool();
            $spool->setMessageLimit($config['flush_msg_limit']);
            $spool->setTimeLimit($config['flush_time_limit']);
            $failures = [];
            $result = $spool->flushQueue(static::getTransport(), $failures);
            return $result . ' messages flushed from queue...';
        } catch (\Exception $e) {
            $grav['log']->error($e->getMessage());
            return $e->getMessage();
        }

    }

    public static function clearQueueFailures()
    {
        $grav = Grav::instance();
        $grav['debugger']->enabled(false);

        $preferences = \Swift_Preferences::getInstance();
        $preferences->setTempDir(sys_get_temp_dir());

        /** @var \Swift_Transport $transport */
        $transport = static::getTransport();
        if (!$transport->isStarted()) {
            $transport->start();
        }

        $queue_path = static::getQueuePath();

        foreach (new \GlobIterator($queue_path . '/*.sending') as $file) {
            $final_message = $file->getPathname();

            /** @var \Swift_Message $message */
            $message = unserialize(file_get_contents($final_message));

            echo(sprintf(
                'Retrying "%s" to "%s"',
                $message->getSubject(),
                implode(', ', array_keys($message->getTo()))
            ) . "\n");

            try {
                $clean = static::cloneMessage($message);
                $transport->send($clean);
                echo("sent!\n");

                // DOn't want to trip up any errors from sending too fast
                sleep(1);
            } catch (\Swift_TransportException $e) {
                echo("ERROR: Send failed - deleting spooled message\n");
            }

            // Remove the file
            unlink($final_message);
        }
    }

    /**
     * Clean copy a message
     *
     * @param \Swift_Message $message
     */
    public static function cloneMessage($message)
    {
        $clean = new \Swift_Message();

        $clean->setBoundary($message->getBoundary());
        $clean->setBcc($message->getBcc());
        $clean->setBody($message->getBody());
        $clean->setCharset($message->getCharset());
        $clean->setChildren($message->getChildren());
        $clean->setContentType($message->getContentType());
        $clean->setCc($message->getCc());
        $clean->setDate($message->getDate());
        $clean->setDescription($message->getDescription());
        $clean->setEncoder($message->getEncoder());
        $clean->setFormat($message->getFormat());
        $clean->setFrom($message->getFrom());
        $clean->setId($message->getId());
        $clean->setMaxLineLength($message->getMaxLineLength());
        $clean->setPriority($message->getPriority());
        $clean->setReplyTo($message->getReplyTo());
        $clean->setReturnPath($message->getReturnPath());
        $clean->setSender($message->getSender());
        $clean->setSubject($message->getSubject());
        $clean->setTo($message->getTo());
        $clean->setAuthMode($message->getAuthMode());

        return $clean;

    }

    protected static function getTransport()
    {
        /** @var Config $config */
        $config = Grav::instance()['config'];

        $engine = $config->get('plugins.email.mailer.engine');

        // Create the Transport and initialize it.
        switch ($engine) {
            case 'smtp':
                $transport = new \Swift_SmtpTransport();

                $options = $config->get('plugins.email.mailer.smtp');
                if (!empty($options['server'])) {
                    $transport->setHost($options['server']);
                }
                if (!empty($options['port'])) {
                    $transport->setPort($options['port']);
                }
                if (!empty($options['encryption']) && $options['encryption'] !== 'none') {
                    $transport->setEncryption($options['encryption']);
                }
                if (!empty($options['user'])) {
                    $transport->setUsername($options['user']);
                }
                if (!empty($options['password'])) {
                    $transport->setPassword($options['password']);
                }
                if (!empty($options['auth_mode'])) {
                    $transport->setAuthMode($options['auth_mode']);
                }
                break;
            case 'sendmail':
            default:
                $options = $config->get('plugins.email.mailer.sendmail');
                $bin = !empty($options['bin']) ? $options['bin'] : '/usr/sbin/sendmail';
                $transport = new \Swift_SendmailTransport($bin);
                break;
        }

        return $transport;
    }
}
