<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Newsletter\Model;

use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\MailException;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * Class to update newsletter subscription status
 */
class SubscriptionManager implements SubscriptionManagerInterface
{
    /**
     * @var SubscriberFactory
     */
    private $subscriberFactory;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var StoreManagerInterface
     */
    private $storeManager;

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var AccountManagementInterface
     */
    private $customerAccountManagement;

    /**
     * @var CustomerRepositoryInterface
     */
    private $customerRepository;

    /**
     * @param SubscriberFactory $subscriberFactory
     * @param LoggerInterface $logger
     * @param StoreManagerInterface $storeManager
     * @param ScopeConfigInterface $scopeConfig
     * @param AccountManagementInterface $customerAccountManagement
     * @param CustomerRepositoryInterface $customerRepository
     */
    public function __construct(
        SubscriberFactory $subscriberFactory,
        LoggerInterface $logger,
        StoreManagerInterface $storeManager,
        ScopeConfigInterface $scopeConfig,
        AccountManagementInterface $customerAccountManagement,
        CustomerRepositoryInterface $customerRepository
    ) {
        $this->subscriberFactory = $subscriberFactory;
        $this->logger = $logger;
        $this->storeManager = $storeManager;
        $this->scopeConfig = $scopeConfig;
        $this->customerAccountManagement = $customerAccountManagement;
        $this->customerRepository = $customerRepository;
    }

    /**
     * @inheritdoc
     */
    public function subscribe(string $email, int $storeId): Subscriber
    {
        $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId();
        $subscriber = $this->subscriberFactory->create()->loadBySubscriberEmail($email, $websiteId);
        $currentStatus = (int)$subscriber->getStatus();
        if ($currentStatus === Subscriber::STATUS_SUBSCRIBED) {
            return $subscriber;
        }

        $status = $this->isConfirmNeed($storeId) ? Subscriber::STATUS_NOT_ACTIVE : Subscriber::STATUS_SUBSCRIBED;
        if (!$subscriber->getId()) {
            $subscriber->setSubscriberConfirmCode($subscriber->randomSequence());
            $subscriber->setSubscriberEmail($email);
        }
        $subscriber->setStatus($status)
            ->setStoreId($storeId)
            ->save();

        $this->sendEmailAfterChangeStatus($subscriber);

        return $subscriber;
    }

    /**
     * @inheritdoc
     */
    public function unsubscribe(string $email, int $storeId, string $confirmCode): Subscriber
    {
        $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId();
        /** @var Subscriber $subscriber */
        $subscriber = $this->subscriberFactory->create()->loadBySubscriberEmail($email, $websiteId);
        if (!$subscriber->getId()) {
            return $subscriber;
        }
        $subscriber->setCheckCode($confirmCode);
        $subscriber->unsubscribe();

        return $subscriber;
    }

    /**
     * @inheritdoc
     */
    public function subscribeCustomer(int $customerId, int $storeId): Subscriber
    {
        return $this->updateCustomerSubscription($customerId, $storeId, true);
    }

    /**
     * @inheritdoc
     */
    public function unsubscribeCustomer(int $customerId, int $storeId): Subscriber
    {
        return $this->updateCustomerSubscription($customerId, $storeId, false);
    }

    /**
     * Update customer newsletter subscription
     *
     * @param int $customerId
     * @param int $storeId
     * @param bool $status
     * @return Subscriber
     */
    private function updateCustomerSubscription(int $customerId, int $storeId, bool $status): Subscriber
    {
        $customer = $this->customerRepository->getById($customerId);
        $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId();
        $subscriber = $this->loadSubscriberByCustomer($customer, $websiteId);
        if (!$status && !$subscriber->getId()) {
            return $subscriber;
        }

        $newStatus = $this->getNewSubscriptionStatus($subscriber, $customer, $storeId, $status);
        $needToSendLetter = $this->saveSubscriber($subscriber, $customer, $storeId, $newStatus);
        if ($needToSendLetter) {
            $this->sendEmailAfterChangeStatus($subscriber);
        }

        return $subscriber;
    }

    /**
     * Load subscriber model by customer
     *
     * @param CustomerInterface $customer
     * @param int $websiteId
     * @return Subscriber
     */
    private function loadSubscriberByCustomer(CustomerInterface $customer, int $websiteId): Subscriber
    {
        $subscriber = $this->subscriberFactory->create();
        $subscriber->loadByCustomer((int)$customer->getId(), $websiteId);
        if (!$subscriber->getId()) {
            $subscriber->loadBySubscriberEmail((string)$customer->getEmail(), $websiteId);
        }

        return $subscriber;
    }

    /**
     * Save Subscriber model
     *
     * @param Subscriber $subscriber
     * @param CustomerInterface $customer
     * @param int $storeId
     * @param int $status
     * @return bool Need to send email
     */
    private function saveSubscriber(
        Subscriber $subscriber,
        CustomerInterface $customer,
        int $storeId,
        int $status
    ): bool {
        $statusChanged = (int)$subscriber->getStatus() !== $status;
        $emailChanged = $subscriber->getEmail() !== $customer->getEmail();
        if ($subscriber->getId()
            && !$statusChanged
            && (int)$subscriber->getCustomerId() === (int)$customer->getId()
            && (int)$subscriber->getStoreId() === $storeId
            && !$emailChanged
        ) {
            return false;
        }

        if (!$subscriber->getId()) {
            $subscriber->setSubscriberConfirmCode($subscriber->randomSequence());
        }
        $subscriber->setStatus($status)
            ->setStatusChanged($statusChanged)
            ->setCustomerId($customer->getId())
            ->setStoreId($storeId)
            ->setEmail($customer->getEmail())
            ->save();

        if ($statusChanged) {
            return true;
        }

        /**
         * If the subscriber is waiting to confirm from the customer
         * and customer changed the email
         * than need to send confirmation letter to the new email
         */
        return $status === Subscriber::STATUS_NOT_ACTIVE && $emailChanged;
    }

    /**
     * Get new subscription status
     *
     * @param Subscriber $subscriber
     * @param CustomerInterface $customer
     * @param int $storeId
     * @param bool $subscribe
     * @return int
     */
    private function getNewSubscriptionStatus(
        Subscriber $subscriber,
        CustomerInterface $customer,
        int $storeId,
        bool $subscribe
    ): int {
        $currentStatus = (int)$subscriber->getStatus();
        // If the current status is already as needed then return them
        if (($subscribe && $currentStatus === Subscriber::STATUS_SUBSCRIBED)
            || (!$subscribe && $currentStatus === Subscriber::STATUS_UNSUBSCRIBED)
        ) {
            return $currentStatus;
        }

        $status = $currentStatus;
        if ($subscribe) {
            $customerConfirmStatus = $this->customerAccountManagement->getConfirmationStatus($customer->getId());
            if ($customerConfirmStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) {
                $status = Subscriber::STATUS_UNCONFIRMED;
            } elseif ($this->isConfirmNeed($storeId)) {
                $status = Subscriber::STATUS_NOT_ACTIVE;
            } else {
                $status = Subscriber::STATUS_SUBSCRIBED;
            }
        } elseif ($currentStatus === Subscriber::STATUS_SUBSCRIBED) {
            $status = Subscriber::STATUS_UNSUBSCRIBED;
        }

        return $status;
    }

    /**
     * Sends out email to customer after change subscription status
     *
     * @param Subscriber $subscriber
     * @return void
     */
    private function sendEmailAfterChangeStatus(Subscriber $subscriber): void
    {
        $status = (int)$subscriber->getStatus();
        if ($status === Subscriber::STATUS_UNCONFIRMED) {
            return;
        }

        try {
            switch ($status) {
                case Subscriber::STATUS_UNSUBSCRIBED:
                    $subscriber->sendUnsubscriptionEmail();
                    break;
                case Subscriber::STATUS_SUBSCRIBED:
                    $subscriber->sendConfirmationSuccessEmail();
                    break;
                case Subscriber::STATUS_NOT_ACTIVE:
                    $subscriber->sendConfirmationRequestEmail();
                    break;
            }
        } catch (MailException $e) {
            // If we are not able to send a new account email, this should be ignored
            $this->logger->critical($e);
        }
    }

    /**
     * Is need to confirm subscription
     *
     * @param int $storeId
     * @return bool
     */
    private function isConfirmNeed(int $storeId): bool
    {
        return (bool)$this->scopeConfig->isSetFlag(
            Subscriber::XML_PATH_CONFIRMATION_FLAG,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }
}
