<?php
/***********************************************************************************
 * The contents of this file are subject to the Extension License Agreement
 * ("Agreement") which can be viewed at
 * https://www.espocrm.com/extension-license-agreement/.
 * By copying, installing downloading, or using this file, You have unconditionally
 * agreed to the terms and conditions of the Agreement, and You may not use this
 * file except in compliance with the Agreement. Under the terms of the Agreement,
 * You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
 * redistribute, market, publish, commercialize, or otherwise transfer rights or
 * usage to the software or any modified version or derivative work of the software
 * created by or for you.
 *
 * Copyright (C) 2015-2025 Letrium Ltd.
 *
 * License ID: e4c270586a0c8a9fda53bda910f357e0
 ************************************************************************************/

namespace Espo\Modules\Advanced\Tools\Workflow;

use Espo\Core\Mail\Exceptions\NoSmtp;
use Espo\Core\Mail\Sender;
use Espo\Core\Mail\SenderParams;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Tools\EmailTemplate\Result;
use Laminas\Mail\Message;

use Espo\Core\Mail\Account\GroupAccount\AccountFactory as GroupAccountFactory;
use Espo\Core\Mail\Account\PersonalAccount\AccountFactory as PersonalAccountFactory;
use Espo\Core\InjectableFactory;
use Espo\Core\Exceptions\Error;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\ServiceFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Hasher;
use Espo\Core\Utils\Language;
use Espo\Entities\Attachment;
use Espo\Entities\Email;
use Espo\Entities\EmailAccount;
use Espo\Entities\EmailTemplate;
use Espo\Entities\InboundEmail;
use Espo\Entities\User;
use Espo\Modules\Advanced\Core\Workflow\Helper;
use Espo\Modules\Advanced\Entities\BpmnProcess as BpmnProcessEntity;
use Espo\Modules\Advanced\Entities\Workflow as WorkflowEntity;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Services\EmailAccount as EmailAccountService;
use Espo\Services\InboundEmail as InboundEmailService;
use Espo\Tools\EmailTemplate\Processor as EmailTemplateProcessor;
use Espo\Tools\EmailTemplate\Data as EmailTemplateData;
use Espo\Tools\EmailTemplate\Params as EmailTemplateParams;

use RuntimeException;
use Exception;
use stdClass;

class SendEmailService
{
    public function __construct(
        private EntityManager $entityManager,
        private ServiceContainer $recordServiceContainer,
        private Config $config,
        private ServiceFactory $serviceFactory,
        private Helper $workflowHelper,
        private EmailSender $emailSender,
        private Hasher $hasher,
        private Language $defaultLanguage,
        private EmailTemplateProcessor $emailTemplateProcessor,
        private InjectableFactory $injectableFactory
    ) {}

    /**
     * Send email for a workflow.
     * @return bool|string
     * @throws Error
     * @throws NoSmtp
     * @todo Introduce SendEmailData class.
     */
    public function send(stdClass $data)
    {
        $workflowId = $data->workflowId;

        if (!$this->validateSendEmailData($data)) {
            throw new Error("Workflow[$workflowId][sendEmail]: Email data is invalid.");
        }

        if ($workflowId) {
            $workflow = $this->entityManager->getRDBRepositoryByClass(WorkflowEntity::class)->getById($workflowId);

            if (!$workflow) {
                return false;
            }

            if (!$workflow->isActive()) {
                return false;
            }
        }

        /** @var string[] $attachmentIds */
        $attachmentIds = $data->attachmentIds ?? [];

        $entity = null;

        if (!empty($data->entityType) && !empty($data->entityId)) {
            $entity = $this->entityManager->getEntityById($data->entityType, $data->entityId);
        }

        if (!$entity) {
            throw new Error("Workflow[$workflowId][sendEmail]: Target Entity is not found.");
        }

        $entityService = $this->recordServiceContainer->get($entity->getEntityType());

        $entityService->loadAdditionalFields($entity);

        $toEmailAddress = $this->getEmailAddress($data->to);
        $fromEmailAddress = $this->getEmailAddress($data->from);

        $replyToEmailAddress = null;

        if (!empty($data->replyTo)) {
            $replyToEmailAddress = $this->getEmailAddress($data->replyTo);
        }

        $ccEmailAddress = null;

        if (!empty($data->cc)) {
            $ccEmailAddress = $this->getEmailAddress($data->cc);
        }

        if (empty($toEmailAddress)) {
            throw new Error("Workflow[$workflowId][sendEmail]: To email address is empty.");
        }

        if (empty($fromEmailAddress)) {
            throw new Error("Workflow[$workflowId][sendEmail]: From email address is empty or could not be obtained.");
        }

        /** @var array<string, Entity> $entityHash */
        $entityHash = [$data->entityType => $entity];

        if (
            isset($data->to->entityType) &&
            isset($data->to->entityId) &&
            $data->to->entityType !== $data->entityType
        ) {
            /** @var string $toEntityType */
            $toEntityType = $data->to->entityType;

            $e = $this->entityManager->getEntityById($toEntityType, $data->to->entityId);

            if ($e) {
                $entityHash[$toEntityType] = $e;
            }
        }

        $fromName = null;

        if (
            isset($data->from->entityType) &&
            isset($data->from->entityId) &&
            $data->from->entityType === User::ENTITY_TYPE
        ) {
            $user = $this->entityManager
                ->getRDBRepositoryByClass(User::class)
                ->getById($data->from->entityId);

            if ($user) {
                $entityHash[User::ENTITY_TYPE] = $user;

                $fromName = $user->getName();
            }
        }

        $templateResult = $this->getTemplateResult($data, $entityHash, $toEmailAddress, $entity);

        $subject = $templateResult->getSubject();
        $body = $templateResult->getBody();

        if (isset($data->variables)) {
            foreach (get_object_vars($data->variables) as $key => $value) {
                if (is_string($value) || is_int($value) || is_float($value)) {
                    if (is_int($value) || is_float($value)) {
                        $value = strval($value);
                    } else if (!$value) {
                        continue;
                    }

                    $subject = str_replace('{$$' . $key . '}', $value, $subject);
                    $body = str_replace('{$$' . $key . '}', $value, $body);
                }
            }
        }

        $body = $this->applyTrackingUrlsToEmailBody($body, $toEmailAddress);

        $sender = $this->emailSender->create();

        if ($data->optOutLink ?? false) {
            $body = $this->applyOptOutLink($toEmailAddress, $body, $templateResult, $sender);
        }

        $emailData = [
            'from' => $fromEmailAddress,
            'to' => $toEmailAddress,
            'cc' => $ccEmailAddress,
            'subject' => $subject,
            'body' => $body,
            'isHtml' => $templateResult->isHtml(),
            'parentId' => $entity->getId(),
            'parentType' => $entity->getEntityType(),
        ];

        if ($replyToEmailAddress) {
            $emailData['replyTo'] = $replyToEmailAddress;
        }

        if (isset($fromName)) {
            $emailData['fromName'] = $fromName;
        }

        $email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();

        $email->set($emailData);

        $attachmentList = $this->getAttachmentList($templateResult, $attachmentIds);

        if (!$data->doNotStore) {
            $email->set('attachmentsIds', $templateResult->getAttachmentIdList());
        }

        $smtpParams = $this->prepareSmtpParams($data, $fromEmailAddress);

        if ($smtpParams) {
            $sender->withSmtpParams($smtpParams);
        }

        $sender->withAttachments($attachmentList);

        if ($replyToEmailAddress) {
            $senderParams = SenderParams::create()->withReplyToAddress($replyToEmailAddress);

            $sender->withParams($senderParams);
        }

        $sendExceptionMessage = null;

        try {
            $sender->send($email);
        } catch (Exception $e) {
            $sendExceptionMessage = $e->getMessage();
        }

        if (isset($sendExceptionMessage)) {
            throw new Error("Workflow[$workflowId][sendEmail]: $sendExceptionMessage.");
        }

        if (!($data->doNotStore ?? false)) {
            $this->storeEmail($email, $data);

            if ($data->returnEmailId ?? false) {
                return $email->getId();
            }
        }

        return true;
    }

    private function validateSendEmailData(stdClass $data): bool
    {
        if (
            !isset($data->entityId) ||
            !(isset($data->entityType)) ||
            !isset($data->emailTemplateId) ||
            !isset($data->from) ||
            !isset($data->to)
        ) {
            return false;
        }

        return true;
    }

    private function getEmailAddress(stdClass $data): ?string
    {
        if (isset($data->email)) {
            return $data->email;
        }

        $entityType = $data->entityType ?? $data->entityName ?? null;

        $entity = null;

        if (isset($entityType) && isset($data->entityId)) {
            $entity = $this->entityManager->getEntityById($entityType, $data->entityId);
        }

        $workflowHelper = $this->workflowHelper;

        if (isset($data->type)) {
            switch ($data->type) {
                case 'specifiedTeams':
                    $userIds = $workflowHelper->getUserIdsByTeamIds($data->entityIds);

                    return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));

                case 'teamUsers':
                    if (!$entity instanceof CoreEntity) {
                        return null;
                    }

                    $entity->loadLinkMultipleField('teams');
                    $userIds = $workflowHelper->getUserIdsByTeamIds($entity->get('teamsIds'));

                    return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));

                case 'followers':
                    if (!$entity) {
                        return null;
                    }

                    $userIds = $workflowHelper->getFollowerUserIds($entity);

                    return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));

                case 'followersExcludingAssignedUser':
                    if (!$entity) {
                        return null;
                    }

                    $userIds = $workflowHelper->getFollowerUserIdsExcludingAssignedUser($entity);

                    return implode('; ', $workflowHelper->getUsersEmailAddress($userIds));

                case 'system':
                    return $this->config->get('outboundEmailFromAddress');

                case 'specifiedUsers':
                    return implode('; ', $workflowHelper->getUsersEmailAddress($data->entityIds));

                case 'specifiedContacts':
                    return implode('; ', $workflowHelper->getEmailAddressesForEntity('Contact', $data->entityIds));
            }
        }

        if ($entity instanceof Entity && $entity->hasAttribute('emailAddress')) {
            return $entity->get('emailAddress');
        }

        if (
            isset($data->type) &&
            isset($entityType) &&
            isset($data->entityIds) &&
            is_array($data->entityIds)
        ) {
            return implode('; ', $workflowHelper->getEmailAddressesForEntity($entityType, $data->entityIds));
        }

        return null;
    }

    private function applyTrackingUrlsToEmailBody(string $body, string $toEmailAddress): string
    {
        $siteUrl = $this->config->get('siteUrl');

        if (!str_contains($body, '{trackingUrl:')) {
            return $body;
        }

        $hash = $this->hasher->hash($toEmailAddress);

        preg_match_all('/\{trackingUrl:(.*?)}/', $body, $matches);

        /** @phpstan-ignore-next-line */
        if (!$matches || !count($matches)) {
            return $body;
        }

        foreach ($matches[0] as $item) {
            $id = explode(':', trim($item, '{}'), 2)[1] ?? null;

            if (!$id) {
                continue;
            }

            if (strpos($id, '.')) {
                [$id, $uid] = explode('.', $id);

                $uidHash = $this->hasher->hash($uid);

                $url = "$siteUrl?entryPoint=campaignUrl&id=$id&uid=$uid&hash=$uidHash";
            } else {
                $url = "$siteUrl?entryPoint=campaignUrl&id=$id&emailAddress=$toEmailAddress&hash=$hash";
            }

            $body = str_replace($item, $url, $body);
        }

        return $body;
    }

    /**
     * @throws Error
     * @throws NoSmtp
     * @return ?array<string, mixed>
     */
    private function getUserSmtpParams(string $emailAddress, string $userId): ?array
    {
        $rawSmtpParams = null;

        $user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);

        if (!$user || !$user->get('isActive')) {
            return null;
        }

        $emailAccount = $this->entityManager
            ->getRDBRepository(EmailAccount::ENTITY_TYPE)
            ->where([
                'emailAddress' => $emailAddress,
                'assignedUserId' => $userId,
                'useSmtp' => true,
                'status' => EmailAccount::STATUS_ACTIVE,
            ])
            ->findOne();

        if ($emailAccount) {
            if (class_exists("Espo\\Core\\Mail\\Account\\PersonalAccount\\AccountFactory")) {
                $factory = $this->injectableFactory->create(PersonalAccountFactory::class);

                $params = $factory->create($emailAccount->getId())
                    ->getSmtpParams();

                if ($params) {
                    $rawSmtpParams = $params->toArray();
                }
            } else {
                /** @var EmailAccountService $service */
                $service = $this->serviceFactory->create(EmailAccount::ENTITY_TYPE);

                $rawSmtpParams = $service->getSmtpParamsFromAccount($emailAccount);
            }
        }

        if ($rawSmtpParams) {
            $rawSmtpParams['fromName'] = $user->get('name');
        }

        return $rawSmtpParams ?? null;
    }

    /**
     * @throws Error
     * @throws NoSmtp
     * @return ?array<string, mixed>
     */
    private function getGroupSmtpParams(string $emailAddress): ?array
    {
        $inboundEmail = $this->entityManager
            ->getRDBRepository(InboundEmail::ENTITY_TYPE)
            ->where([
                'status' => InboundEmail::STATUS_ACTIVE,
                'useSmtp' => true,
                'smtpHost!=' => null,
                'emailAddress' => $emailAddress,
            ])
            ->findOne();

        if (!$inboundEmail) {
            return null;
        }

        $rawSmtpParams = null;

        if (class_exists("Espo\\Core\\Mail\\Account\\GroupAccount\\AccountFactory")) {
            $params = $this->injectableFactory
                ->create(GroupAccountFactory::class)
                ->create($inboundEmail->getId())
                ->getSmtpParams();

            if ($params) {
                $rawSmtpParams = $params->toArray();
            }

            return $rawSmtpParams;
        }

        /** @var InboundEmailService $service */
        $service = $this->serviceFactory->create(InboundEmail::ENTITY_TYPE);

        return $service->getSmtpParamsFromAccount($inboundEmail);
    }

    /**
     * @param Result $templateResult
     * @param string[] $attachmentIds
     * @return Attachment[]
     */
    private function getAttachmentList(Result $templateResult, array $attachmentIds): array
    {
        $attachmentList = [];

        foreach (array_merge($templateResult->getAttachmentIdList(), $attachmentIds) as $attachmentId) {
            $attachment = $this->entityManager
                ->getRDBRepositoryByClass(Attachment::class)
                ->getById($attachmentId);

            if ($attachment) {
                $attachmentList[] = $attachment;
            }
        }

        return $attachmentList;
    }

    private function storeEmail(Email $email, stdClass $data): void
    {
        $processId = $data->processId ?? null;
        $emailTemplateId = $data->emailTemplateId ?? null;

        $teamsIds = [];

        if ($processId) {
            $process = $this->entityManager
                ->getRDBRepositoryByClass(BpmnProcessEntity::class)
                ->getById($processId);

            if ($process) {
                $teamsIds = $process->getLinkMultipleIdList('teams');
            }
        } else if ($emailTemplateId) {
            $emailTemplate = $this->entityManager
                ->getRDBRepositoryByClass(EmailTemplate::class)
                ->getById($emailTemplateId);

            if ($emailTemplate) {
                $teamsIds = $emailTemplate->getLinkMultipleIdList('teams');
            }
        }

        if (count($teamsIds)) {
            $email->set('teamsIds', $teamsIds);
        }

        $this->entityManager->saveEntity($email, ['createdById' => 'system']);
    }

    /**
     * @return ?array<string, mixed>
     * @throws Error
     * @throws NoSmtp
     */
    private function prepareSmtpParams(stdClass $data, string $fromEmailAddress): ?array
    {
        $smtpParams = null;

        if (
            isset($data->from->entityType) &&
            $data->from->entityType == User::ENTITY_TYPE &&
            isset($data->from->entityId)
        ) {
            $smtpParams = $this->getUserSmtpParams($fromEmailAddress, $data->from->entityId);
        } else if (isset($data->from->email)) {
            $smtpParams = $this->getGroupSmtpParams($fromEmailAddress);
        }

        return $smtpParams;
    }

    private function getEmailTemplate(stdClass $data): EmailTemplate
    {
        $emailTemplateId = $data->emailTemplateId ?? null;

        if (!$emailTemplateId) {
            throw new RuntimeException("No email template.");
        }

        $emailTemplate = $this->entityManager
            ->getRDBRepositoryByClass(EmailTemplate::class)
            ->getById($emailTemplateId);

        if (!$emailTemplate) {
            throw new RuntimeException("Email template $emailTemplateId not found.");
        }

        return $emailTemplate;
    }

    /**
     * @param array<string, Entity> $entityHash
     * @return Result
     */
    private function getTemplateResult(
        stdClass $data,
        array $entityHash,
        string $toEmailAddress,
        Entity $entity
    ): Result {

        $emailTemplate = $this->getEmailTemplate($data);

        $emailTemplateData = EmailTemplateData::create()
            ->withEntityHash($entityHash)
            ->withEmailAddress($toEmailAddress)
            ->withParentId($entity->getId())
            ->withParentType($entity->getEntityType());

        if (
            $entity->hasAttribute('parentId') &&
            $entity->hasAttribute('parentType')
        ) {
            $emailTemplateData = $emailTemplateData
                ->withRelatedId($entity->get('parentId'))
                ->withRelatedType($entity->get('parentType'));
        }

        return $this->emailTemplateProcessor->process(
            $emailTemplate,
            EmailTemplateParams::create()->withCopyAttachments(),
            $emailTemplateData
        );
    }

    private function applyOptOutLink(
        string $toEmailAddress,
        string $body,
        Result $templateResult,
        Sender $sender,
    ): string {

        $siteUrl = $this->config->get('siteUrl');

        $hash = $this->hasher->hash($toEmailAddress);

        $optOutUrl = "$siteUrl?entryPoint=unsubscribe&emailAddress=$toEmailAddress&hash=$hash";

        $optOutLink = "<a href=\"$optOutUrl\">" .
            "{$this->defaultLanguage->translateLabel('Unsubscribe', 'labels', 'Campaign')}</a>";

        $body = str_replace('{optOutUrl}', $optOutUrl, $body);
        $body = str_replace('{optOutLink}', $optOutLink, $body);

        if (stripos($body, '?entryPoint=unsubscribe') === false) {
            if ($templateResult->isHtml()) {
                $body .= "<br><br>" . $optOutLink;
            } else {
                $body .= "\n\n" . $optOutUrl;
            }
        }

        if (method_exists($sender, 'withAddedHeader')) { /** @phpstan-ignore-line */
            $sender->withAddedHeader('List-Unsubscribe', '<' . $optOutUrl . '>');
        } else {
            $message = new Message();
            $message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>');

            $sender->withMessage($message);
        }

        return $body;
    }
}
