<?php

/**
 * @package   bdk/debug
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2025 Brad Kent
 * @since     2.3
 */

namespace bdk\Debug\Collector;

use bdk\Debug;
use bdk\PubSub\Manager as EventManager;
use SplObjectStorage;
use Swift_Events_CommandEvent;
use Swift_Events_CommandListener;
use Swift_Events_ResponseEvent;
use Swift_Events_ResponseListener;
use Swift_Events_SendEvent;
use Swift_Events_SendListener;
use Swift_Events_TransportChangeEvent;
use Swift_Events_TransportChangeListener;
use Swift_Events_TransportExceptionEvent;
use Swift_Events_TransportExceptionListener;
use Swift_Plugins_Logger;
use Swift_TransportException;

/**
 * A SwiftMailer adapter
 */
class SwiftMailerLogger implements
    Swift_Events_CommandListener,
    Swift_Events_ResponseListener,
    Swift_Events_SendListener,
    Swift_Events_TransportChangeListener,
    Swift_Events_TransportExceptionListener,
    Swift_Plugins_Logger
{
    /** @var list<string> */
    protected $messages = array();

    /** @var string */
    protected $icon = ':email:';

    /** @var string */
    protected $iconMeta;

    /** @var bool */
    protected $useIcon = true;

    /** @var SplObjectStorage */
    protected $transports;

    /** @var Debug */
    private $debug;

    /**
     * Constructor
     *
     * @param Debug|null $debug (optional) Specify Debug instance
     *                           if not passed, will create Slim channel on singleton instance
     *                           if root channel is specified, will create a SwiftMailer channel
     *
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    public function __construct($debug = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug');

        $channelKey = 'SwiftMailer';
        $channelOptions = array(
            'channelIcon' => $this->icon,
            'channelName' => 'SwiftMailer',
        );
        if (!$debug) {
            $debug = Debug::getChannel($channelKey, $channelOptions);
        } elseif ($debug === $debug->rootInstance) {
            $debug = $debug->getChannel($channelKey, $channelOptions);
        }
        $debug->rootInstance->eventManager->subscribe(EventManager::EVENT_PHP_SHUTDOWN, [$this, 'onShutdown'], PHP_INT_MAX * -1 + 1);
        $this->debug = $debug;
        $this->iconMeta = $this->debug->meta('icon', $this->icon);
        $this->transports = new SplObjectStorage();
    }

    /**
     * Invoked immediately before the Message is sent.
     *
     * Implements Swift_Events_SendListener
     *
     * @param Swift_Events_SendEvent $event Swift SendEvent instance
     *
     * @return void
     */
    public function beforeSendPerformed(Swift_Events_SendEvent $event)
    {
        $msg = $event->getMessage();
        $this->debug->groupCollapsed(
            $this->debug->i18n->trans('email.sending'),
            $this->formatEmailAddresses($msg->getTo()),
            $msg->getSubject(),
            $this->iconMeta
        );
        $this->debug->log($this->debug->i18n->trans('headers'), $msg->getHeaders()->toString());
        $this->useIcon = false; // don't use icon within group;
    }

    /**
     * Invoked immediately after the Message is sent.
     *
     * Implements Swift_Events_SendListener
     *
     * @param Swift_Events_SendEvent $event Swift SendEvent Instance
     *
     * @return void
     */
    public function sendPerformed(Swift_Events_SendEvent $event)
    {
        array($event);
        $this->debug->groupEnd();
        $this->clear();
        $this->useIcon = true;
    }

    /**
     * Add a log entry.
     *
     * Implements Swift_Plugins_Logger
     *
     * @param string $entry message
     *
     * @return void
     */
    public function add($entry)
    {
        $this->messages[] = $entry;
        $debugArgs = array($entry);
        $matches = array();
        if (\preg_match('#^(([-+><])\2) (.+)$#s', $entry, $matches)) {
            $debugArgs = [$matches[1] . ':', $matches[3]];
        }
        if ($this->useIcon) {
            $debugArgs[] = $this->iconMeta;
        }
        \call_user_func_array([$this->debug, 'log'], $debugArgs);
    }

    /**
     * Clear the log contents.
     *
     * Implements Swift_Plugins_Logger
     *
     * @return void
     */
    public function clear()
    {
        $this->messages = array();
    }

    /**
     * Get this log as a string.
     *
     * Implements Swift_Plugins_Logger
     *
     * @return string
     */
    public function dump()
    {
        return \implode(PHP_EOL, $this->messages);
    }

    /**
     * Invoked immediately following a command being sent.
     *
     * Implements Swift_Events_CommandListener
     *
     * @param Swift_Events_CommandEvent $evt Swift Event
     *
     * @return void
     */
    public function commandSent(Swift_Events_CommandEvent $evt)
    {
        $command = $evt->getCommand();
        $this->add(\sprintf('>> %s', $command));
    }

    /**
     * Invoked immediately following a response coming back.
     *
     * Implements Swift_Events_ResponseListener
     *
     * @param Swift_Events_ResponseEvent $evt Swift Event
     *
     * @return void
     */
    public function responseReceived(Swift_Events_ResponseEvent $evt)
    {
        $response = $evt->getResponse();
        $this->add(\sprintf('<< %s', $response));
    }

    /**
     * Invoked just before a Transport is started.
     *
     * Implements Swift_Events_TransportChangeListener
     *
     * @param Swift_Events_TransportChangeEvent $evt Swift Event
     *
     * @return void
     */
    public function beforeTransportStarted(Swift_Events_TransportChangeEvent $evt)
    {
        $transportName = \get_class($evt->getSource());
        $this->add(\sprintf('++ Starting %s', $transportName));
    }

    /**
     * Invoked just before a Transport is stopped.
     *
     * Implements Swift_Events_TransportChangeListener
     *
     * @param Swift_Events_TransportChangeEvent $evt Swift Event
     *
     * @return void
     */
    public function beforeTransportStopped(Swift_Events_TransportChangeEvent $evt)
    {
        $transportName = \get_class($evt->getSource());
        $this->add(\sprintf('-- Stopping %s', $transportName));
    }

    /**
     * EventManager::EVENT_PHP_SHUTDOWN listener
     *
     * "preemptively" stop transports rather than wait for transport's __destruct
     *
     * @return void
     */
    public function onShutdown()
    {
        foreach ($this->transports as $transport) {
            $transport->stop();
        }
    }

    /**
     * Invoked immediately after the Transport is started.
     *
     * Implements Swift_Events_TransportChangeListener
     *
     * @param Swift_Events_TransportChangeEvent $evt Swift Event
     *
     * @return void
     */
    public function transportStarted(Swift_Events_TransportChangeEvent $evt)
    {
        $transportName = \get_class($evt->getSource());
        $this->add(\sprintf('++ %s started', $transportName));
        $this->clear();
        $this->transports->attach($evt->getSource());
    }

    /**
     * Invoked immediately after the Transport is stopped.
     *
     * Implements Swift_Events_TransportChangeListener
     *
     * @param Swift_Events_TransportChangeEvent $evt Swift Event
     *
     * @return void
     */
    public function transportStopped(Swift_Events_TransportChangeEvent $evt)
    {
        $transportName = \get_class($evt->getSource());
        $this->add(\sprintf('-- %s stopped', $transportName));
        $this->transports->detach($evt->getSource());
    }

    /**
     * Invoked as a TransportException is thrown in the Transport system.
     *
     * Implements Swift_Events_TransportExceptionListener
     *
     * @param Swift_Events_TransportExceptionEvent $evt Swift Event
     *
     * @return void
     * @throws Swift_TransportException
     */
    public function exceptionThrown(Swift_Events_TransportExceptionEvent $evt)
    {
        $exception = $evt->getException();
        $message = $exception->getMessage();
        $code = (int) $exception->getCode();
        $this->debug->warn($code . ':', $message);
        $message .= PHP_EOL;
        $message .= 'Log data:' . PHP_EOL;
        $message .= $this->dump();
        $evt->cancelBubble();
        /** @psalm-suppress ArgumentTypeCoercion ignore 3rd argument expects Exception, but Throwable passed */
        throw new Swift_TransportException($message, $code, $exception->getPrevious());
    }

    /**
     * Convert array of email addresses/names to string
     *
     * @param array $addresses emailAddr->name pairs
     *
     * @return string
     */
    protected function formatEmailAddresses($addresses)
    {
        $return = array();
        foreach ($addresses as $addr => $name) {
            $return[] = ($name ? $name . ' ' : '') . '<' . $addr . '>';
        }
        return \implode(', ', $return);
    }
}
