<?php

/**
 * This file contains \QUI\Mail\Mailer
 */

namespace QUI\Mail;

use Exception;
use Html2Text\Html2Text;
use PHPMailer\PHPMailer\PHPMailer;
use QUI;
use QUI\Projects\Media\Utils as MediaUtils;
use QUI\Projects\Project;

use function explode;
use function file_exists;
use function filesize;
use function is_array;
use function preg_replace;
use function sprintf;
use function str_replace;
use function trim;

/**
 * Mailer class sends a mail
 * It's the main mail wrapper for the php mailer
 *
 * if you want to send a mail, look at \QUI::getMailManager()->send() first
 */
class Mailer extends QUI\QDOM
{
    public const DEFAULT_MAX_ATTACHMENT_SIZE_MB = 50;

    /**
     * Static flag in the Mailer class to control the runtime behavior of mail sending.
     *
     * @var bool $DISABLE_MAIL_SENDING Indicates whether mail sending is enabled (false) or disabled (true) during runtime.
     */
    public static bool $DISABLE_MAIL_SENDING = false;

    /**
     * Mail template
     */
    public ?Template $Template = null;

    /**
     * list of recipients
     *
     * @var array<int, string|array<int, string>>
     */
    protected array $recipients = [];

    /**
     * list of reply
     *
     * @var array<int, string|array<int, string>>
     */
    protected array $reply = [];

    /**
     * list of cc
     *
     * @var array<int, string|array<int, string>>
     */
    protected array $cc = [];

    /**
     * list of bcc
     *
     * @var array<int, string|array<int, string>>
     */
    protected array $bcc = [];

    /**
     * list of attachments
     *
     * @var array<int, string>
     */
    protected array $attachments = [];

    /**
     * @param array<string, mixed> $attributes
     *
     * @throws QUI\Exception
     */
    public function __construct(array $attributes = [])
    {
        $config = QUI::conf('mail');

        if (isset($attributes['Project']) && $attributes['Project'] instanceof Project) {
            $project = $attributes['Project'];
        } else {
            $project = QUI::getProjectManager()->get();

            if (QUI::getRewrite()->getProject()) {
                $project = QUI::getRewrite()->getProject();
            }
        }

        // default
        $this->setAttributes([
            'html' => true,
            'Project' => $project
        ]);

        if (isset($config['MAILFrom'])) {
            $this->setFrom($config['MAILFrom']);
        }

        if (isset($config['MAILFromText'])) {
            $this->setFromName($config['MAILFromText']);
        }


        // construct array
        $this->setAttributes($attributes);

        // HTML mail template
        $this->Template = new Template([
            'Project' => $this->getAttribute('Project'),
            'Mailer' => $this
        ]);

        try {
            QUI::getEvents()->fireEvent('mailer', [$this]);
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }
    }

    /**
     * Set from value for the mail
     *
     * @param string $from - mail@domain.net
     */
    public function setFrom(string $from): void
    {
        $this->setAttribute('from', $from);
    }

    /**
     * Set from name for the mail
     *
     * @param string $fromName - Firstname Lastname
     */
    public function setFromName(string $fromName): void
    {
        $this->setAttribute('fromName', $fromName);
    }

    /**
     * setter
     */

    /**
     * Send the mail
     *
     * @throws QUI\Exception|\PHPMailer\PHPMailer\Exception
     */
    public function send(?PHPMailer $PHPMailer = null): bool
    {
        if (Mailer::$DISABLE_MAIL_SENDING) {
            return true;
        }

        try {
            QUI::getEvents()->fireEvent('mailerSendInit', [
                $this
            ]);
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }

        if (!$PHPMailer) {
            $PHPMailer = QUI::getMailManager()->getPHPMailer();
        }

        $html = $this->getRenderedBody();

        $PHPMailer->Subject = $this->getAttribute('subject');
        $PHPMailer->Body = $html;

        try {
            QUI::getEvents()->fireEvent('mailerSendBegin', [$this, $PHPMailer]);
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }

        // html ?
        if ($this->getAttribute('html')) {
            $Html2Text = new Html2Text($PHPMailer->Body);
            $PHPMailer->AltBody = $Html2Text->getText();
        }

        // addresses
        foreach ($this->recipients as $email) {
            if (empty($email)) {
                continue;
            }

            if (is_array($email)) {
                $PHPMailer->addAddress($email[0], $email[1]);
                continue;
            }

            $PHPMailer->addAddress($email);
        }

        foreach ($this->reply as $email) {
            if (empty($email)) {
                continue;
            }

            if (is_array($email)) {
                $PHPMailer->addReplyTo($email[0], $email[1]);
                continue;
            }

            $PHPMailer->addReplyTo($email);
        }

        foreach ($this->cc as $email) {
            if (empty($email)) {
                continue;
            }

            if (is_array($email)) {
                $PHPMailer->addCC($email[0], $email[1]);
                continue;
            }

            $PHPMailer->addCC($email);
        }

        foreach ($this->bcc as $email) {
            if (empty($email)) {
                continue;
            }

            if (is_array($email)) {
                $PHPMailer->addBCC($email[0], $email[1]);
                continue;
            }

            $PHPMailer->addBCC($email);
        }

        if ((int)QUI::conf('mail', 'admin_bcc')) {
            $adminBccMails = trim(QUI::conf('mail', 'admin_mail'));
            $adminBccMails = explode(',', $adminBccMails);

            foreach ($adminBccMails as $mail) {
                if (!empty($mail)) {
                    $PHPMailer->addBCC($mail);
                }
            }

            $this->addBCC(QUI::conf('mail', 'admin_mail'));
        }

        // attachments
        foreach ($this->attachments as $file) {
            if (!file_exists($file)) {
                continue;
            }

            $this->assertAttachmentSize($file);

            $info = QUI\Utils\System\File::getInfo($file);

            if (!isset($info['mime_type'])) {
                $info['mime_type'] = 'application/octet-stream';
            }

            try {
                $PHPMailer->addAttachment(
                    $file,
                    $info['basename'],
                    'base64',
                    $info['mime_type']
                );
            } catch (\PHPMailer\PHPMailer\Exception $Exception) {
                throw new QUI\Exception(
                    $Exception->getMessage(),
                    $Exception->getCode()
                );
            }
        }

        try {
            QUI::getEvents()->fireEvent('mailerSend', [$this, $PHPMailer]);
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }

        // with mail queue?
        if (QUI::conf('mail', 'queue')) {
            $Queue = new Queue();
            $id = $Queue->addToQueue($this);

            $Queue->sendById($id);

            return true;
        }

        // no mail queue
        try {
            Log::logSend($PHPMailer);

            $PHPMailer->send();

            Log::logDone($PHPMailer);

            return true;
        } catch (Exception $Exception) {
            $tryings = QUI::getEvents()->fireEvent('onMailSendError', [$this, $PHPMailer, $Exception]);

            foreach ($tryings as $try) {
                if ($try) {
                    return true;
                }
            }

            Log::logException($Exception);

            throw new QUI\Exception(
                'Mail Error: ' . $Exception->getMessage()
            );
        }
    }

    /**
     * Add reply to address
     *
     * @param string $email - E-Mail
     * @param false|string $name - E-Mail Name
     */
    public function addReplyTo(string $email, false | string $name = false): void
    {
        $email = trim($email);
        $email = explode(',', $email);

        foreach ($email as $mail) {
            if ($name) {
                $this->reply[] = [$mail, $name];
                continue;
            }

            $this->reply[] = $mail;
        }
    }

    /**
     * Add cc address
     *
     * @param string $email - E-Mail
     * @param false|string $name - E-Mail Name
     */
    public function addCC(string $email, false | string $name = false): void
    {
        $email = trim($email);
        $email = explode(',', $email);

        foreach ($email as $mail) {
            if ($name) {
                $this->cc[] = [$mail, $name];
                continue;
            }

            $this->cc[] = $mail;
        }
    }

    /**
     * Add bcc address
     *
     * @param string $email - E-Mail
     * @param false|string $name - E-Mail Name
     */
    public function addBCC(string $email, false | string $name = false): void
    {
        $email = trim($email);
        $email = explode(',', $email);

        foreach ($email as $mail) {
            if ($name) {
                $this->bcc[] = [$mail, $name];
                continue;
            }

            $this->bcc[] = $mail;
        }
    }

    /**
     * Add a file to the mail
     *
     * @param string $file - path to the file
     * @throws QUI\Exception
     */
    public function addAttachment(string $file): bool
    {
        if (!file_exists($file)) {
            return false;
        }

        $this->assertAttachmentSize($file);

        $this->attachments[] = $file;

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'subject' => $this->getAttribute('subject'),
            'body' => $this->Template->getHTML(),
            'text' => $this->Template->getText(),
            'from' => $this->getAttribute('from'),
            'fromName' => $this->getAttribute('fromName'),
            'ishtml' => $this->getAttribute('html') ? 1 : 0,
            'mailto' => $this->recipients,
            'replyto' => $this->reply,
            'cc' => $this->cc,
            'bcc' => $this->bcc,
            'attachements' => $this->attachments
        ];
    }

    /**
     * add methods
     */
    public function setSubject(string $subject): void
    {
        $this->setAttribute('subject', $subject);
    }

    /**
     * set the HTML flag, is HTML mail or not
     *
     * @param boolean $html - is the mail an HTML mail or not?
     */
    public function setHTML(bool $html): void
    {
        $this->setAttribute('html', $html);
    }

    public function setBody(string $html): void
    {
        $this->Template->setBody(self::prepareBody($html));
    }

    public function getRenderedBody(): string
    {
        $html = $this->Template->getHTML();

        $Output = new QUI\Output();
        $Output->setSetting('use-absolute-urls', true);
        $Output->setSetting('parse-to-picture-elements', false);

        $html = $Output->parse($html);

        $html = preg_replace('#<picture([^>]*)>#i', '', $html);
        $html = preg_replace('#<source([^>]*)>#i', '', $html);

        return str_replace('</picture>', '', $html);
    }

    public static function prepareBody(string $body): string
    {
        preg_match_all('#"(image\.php.*)"#i', $body, $matches);

        if (empty($matches[1])) {
            return $body;
        }

        $baseUrl = QUI::getRewrite()->getProject()->get(1)->getUrlRewrittenWithHost();
        $baseUrl = rtrim($baseUrl, '/');

        foreach ($matches[1] as $mediaUrl) {
            $body = str_replace(
                $mediaUrl,
                $baseUrl . MediaUtils::getRewrittenUrl($mediaUrl),
                $body
            );
        }

        return $body;
    }

    /**
     * Set the project object, for the mailer and the mailer template
     */
    public function setProject(Project $Project): void
    {
        $this->setAttribute('Project', $Project);
        $this->Template->setAttribute('Project', $Project);
    }

    /**
     * Add a recipient
     *
     * @param string $email - E-Mail
     * @param false|string $name - E-Mail Name
     */
    public function addRecipient(string $email, false | string $name = false): void
    {
        $email = trim($email);
        $email = explode(',', $email);

        foreach ($email as $mail) {
            if ($name) {
                $this->recipients[] = [$mail, $name];
                continue;
            }

            $this->recipients[] = $mail;
        }
    }

    /**
     * Add a files to the mail
     *
     * @param array<int, string>|string $files - array with file paths eq:
     *                              addAttachments( array('path/file1.end', 'path/file2.end') )
     *                              addAttachments( 'path/file1.end' )
     * @throws QUI\Exception
     */
    public function addAttachments(array | string $files): void
    {
        if (!is_array($files)) {
            $this->addAttachment($files);

            return;
        }

        foreach ($files as $file) {
            $this->addAttachment($file);
        }
    }

    /**
     * Return the configured max attachment size in bytes.
     */
    public static function getMaxAttachmentSizeInBytes(): int
    {
        $maxAttachmentSize = (int)QUI::conf('mail', 'maxAttachmentSize');

        if ($maxAttachmentSize <= 0) {
            return self::DEFAULT_MAX_ATTACHMENT_SIZE_MB * 1024 * 1024;
        }

        return $maxAttachmentSize;
    }

    /**
     * @throws QUI\Exception
     */
    protected function assertAttachmentSize(string $file): void
    {
        $fileSize = filesize($file);
        $maxFileSize = self::getMaxAttachmentSizeInBytes();

        if ($fileSize === false || $fileSize <= $maxFileSize) {
            return;
        }

        throw new QUI\Exception(
            sprintf(
                'Attachment "%s" exceeds max size of %d MB.',
                $file,
                (int)($maxFileSize / 1024 / 1024)
            )
        );
    }
}
