<?php
/************************************************************************
 * This file is part of EspoCRM.
 *
 * EspoCRM - Open Source CRM application.
 * Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
 * Website: https://www.espocrm.com
 *
 * EspoCRM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * EspoCRM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
 ************************************************************************/

namespace Espo\Services;

use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\NotFound;

use \Espo\Core\Utils\Util;

class App extends \Espo\Core\Services\Base
{
    protected function init()
    {
        $this->addDependency('preferences');
        $this->addDependency('acl');
        $this->addDependency('container');
        $this->addDependency('entityManager');
        $this->addDependency('metadata');
        $this->addDependency('selectManagerFactory');
        $this->addDependency('injectableFactory');
    }

    protected function getPreferences()
    {
        return $this->getInjection('preferences');
    }

    protected function getAcl()
    {
        return $this->getInjection('acl');
    }

    protected function getEntityManager()
    {
        return $this->getInjection('entityManager');
    }

    protected function getMetadata()
    {
        return $this->getInjection('metadata');
    }

    public function getUserData()
    {
        $preferencesData = $this->getPreferences()->getValueMap();
        unset($preferencesData->smtpPassword);

        $settingsService = $this->getServiceFactory()->create('Settings');

        $user = $this->getUser();

        if (!$user->has('teamsIds')) {
            $user->loadLinkMultipleField('teams');
        }
        if ($user->isPortal()) {
            $user->loadAccountField();
            $user->loadLinkMultipleField('accounts');
        }

        $settings = $this->getServiceFactory()->create('Settings')->getConfigData();

        if ($user->get('dashboardTemplateId')) {
            $dashboardTemplate = $this->getEntityManager()->getEntity('DashboardTemplate', $user->get('dashboardTemplateId'));
            if ($dashboardTemplate) {
                $settings->forcedDashletsOptions = $dashboardTemplate->get('dashletsOptions') ?? (object) [];
                $settings->forcedDashboardLayout = $dashboardTemplate->get('layout') ?? [];
            }
        }

        $language = \Espo\Core\Utils\Language::detectLanguage($this->getConfig(), $this->getPreferences());

        $auth2FARequired = false;
        if (
            $user->isRegular() && $this->getConfig()->get('auth2FA') && $this->getConfig()->get('auth2FAForced') &&
            !$user->get('auth2FA')
        ) {
            $auth2FARequired = true;
        }

        $appParams = [
            'maxUploadSize' => $this->getMaxUploadSize() / 1024.0 / 1024.0,
            'isRestrictedMode' => $this->getConfig()->get('restrictedMode'),
            'passwordChangeForNonAdminDisabled' => $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo',
            'timeZoneList' => $this->getMetadata()->get(['entityDefs', 'Settings', 'fields', 'timeZone', 'options'], []),
            'auth2FARequired' => $auth2FARequired,
        ];

        foreach (($this->getMetadata()->get(['app', 'appParams']) ?? []) as $paramKey => $item) {
            $className = $item['className'] ?? null;
            if (!$className) continue;
            try {
                $itemParams = $this->getInjection('injectableFactory')->createByClassName($className)->get();
            } catch (\Throwable $e) {
                $GLOBALS['log']->error("appParam {$paramKey}: " . $e->getMessage());
                continue;
            }
            $appParams[$paramKey] = $itemParams;
        }

        return [
            'user' => $this->getUserDataForFrontend(),
            'acl' => $this->getAclDataForFrontend(),
            'preferences' => $preferencesData,
            'token' => $this->getUser()->get('token'),
            'settings' => $settings,
            'language' => $language,
            'appParams' => $appParams,
        ];
    }

    protected function getUserDataForFrontend()
    {
        $user = $this->getUser();

        $emailAddressData = $this->getEmailAddressData();

        $data = $user->getValueMap();

        $data->emailAddressList = $emailAddressData->emailAddressList;
        $data->userEmailAddressList = $emailAddressData->userEmailAddressList;

        unset($data->authTokenId);
        unset($data->password);

        $forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList('User');

        $isPortal = $user->isPortal();

        foreach ($forbiddenAttributeList as $attribute) {
            if ($attribute === 'type') continue;
            if ($isPortal) {
                if (in_array($attribute, ['contactId', 'contactName', 'accountId', 'accountsIds'])) continue;
            } else {
                if (in_array($attribute, ['teamsIds', 'defaultTeamId', 'defaultTeamName'])) continue;
            }
            unset($data->$attribute);
        }

        return $data;
    }

    protected function getAclDataForFrontend()
    {
        $data = $this->getAcl()->getMap();

        if (!$this->getUser()->isAdmin()) {
            $data = unserialize(serialize($data));

            $scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
            foreach ($scopeList as $scope) {
                if (!$this->getAcl()->check($scope)) {
                    unset($data->table->$scope);
                    unset($data->fieldTable->$scope);
                    unset($data->fieldTableQuickAccess->$scope);
                }
            }
        }

        return $data;
    }

    protected function getEmailAddressData()
    {
        $user = $this->getUser();

        $emailAddressList = [];
        $userEmailAddressList = [];

        foreach ($user->get('emailAddresses') as $emailAddress) {
            if ($emailAddress->get('invalid')) continue;
            $userEmailAddressList[] = $emailAddress->get('name');
            if ($user->get('emailAddress') === $emailAddress->get('name')) continue;
            $emailAddressList[] = $emailAddress->get('name');
        }
        if ($user->get('emailAddress')) {
            array_unshift($emailAddressList, $user->get('emailAddress'));
        }

        $entityManager = $this->getEntityManager();

        $teamIdList = $user->getLinkMultipleIdList('teams');
        $groupEmailAccountPermission = $this->getAcl()->get('groupEmailAccountPermission');
        if ($groupEmailAccountPermission && $groupEmailAccountPermission !== 'no') {
            if ($groupEmailAccountPermission === 'team') {
                if (count($teamIdList)) {
                    $selectParams = [
                        'whereClause' => [
                            'status' => 'Active',
                            'useSmtp' => true,
                            'smtpIsShared' => true,
                            'teamsMiddle.teamId' => $teamIdList
                        ],
                        'joins' => ['teams'],
                        'distinct' => true
                    ];
                    $inboundEmailList = $entityManager->getRepository('InboundEmail')->find($selectParams);
                    foreach ($inboundEmailList as $inboundEmail) {
                        if (!$inboundEmail->get('emailAddress')) continue;
                        $emailAddressList[] = $inboundEmail->get('emailAddress');
                    }
                }
            } else if ($groupEmailAccountPermission === 'all') {
                $selectParams = [
                    'whereClause' => [
                        'status' => 'Active',
                        'useSmtp' => true,
                        'smtpIsShared' => true
                    ]
                ];
                $inboundEmailList = $entityManager->getRepository('InboundEmail')->find($selectParams);
                foreach ($inboundEmailList as $inboundEmail) {
                    if (!$inboundEmail->get('emailAddress')) continue;
                    $emailAddressList[] = $inboundEmail->get('emailAddress');
                }
            }
        }

        return (object) [
            'emailAddressList' => $emailAddressList,
            'userEmailAddressList' => $userEmailAddressList
        ];
    }

    private function getMaxUploadSize()
    {
        $maxSize = 0;

        $postMaxSize = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
        if ($postMaxSize > 0) {
            $maxSize = $postMaxSize;
        }
        $attachmentUploadMaxSize = $this->getConfig()->get('attachmentUploadMaxSize');
        if ($attachmentUploadMaxSize && (!$maxSize || $attachmentUploadMaxSize < $maxSize)) {
            $maxSize = $attachmentUploadMaxSize;
        }

        return $maxSize;
    }

    private function convertPHPSizeToBytes($size)
    {
        if (is_numeric($size)) return $size;

        $suffix = substr($size, -1);
        $value = substr($size, 0, -1);
        switch(strtoupper($suffix)) {
            case 'P':
                $value *= 1024;
            case 'T':
                $value *= 1024;
            case 'G':
                $value *= 1024;
            case 'M':
                $value *= 1024;
            case 'K':
                $value *= 1024;
                break;
            }
        return $value;
    }

    protected function getTemplateEntityTypeList()
    {
        if (!$this->getAcl()->checkScope('Template')) {
            return [];
        }

        $list = [];

        $selectManager = $this->getInjection('selectManagerFactory')->create('Template');

        $selectParams = $selectManager->getEmptySelectParams();
        $selectManager->applyAccess($selectParams);

        $templateList = $this->getEntityManager()->getRepository('Template')
            ->select(['entityType'])
            ->groupBy(['entityType'])
            ->find($selectParams);

        foreach ($templateList as $template) {
            $list[] = $template->get('entityType');
        }

        return $list;
    }

    public function jobClearCache()
    {
        $this->getInjection('container')->get('dataManager')->clearCache();
    }

    public function jobRebuild()
    {
        $this->getInjection('container')->get('dataManager')->rebuild();
    }

    // TODO remove in 5.5.0
    public function jobPopulatePhoneNumberNumeric()
    {
        $numberList = $this->getEntityManager()->getRepository('PhoneNumber')->find();
        foreach ($numberList as $number) {
            $this->getEntityManager()->saveEntity($number);
        }
    }

    // TODO remove in 5.5.0
    public function jobPopulateArrayValues()
    {
        $scopeList = array_keys($this->getMetadata()->get(['scopes']));

        $sql = "DELETE FROM array_value";
        $this->getEntityManager()->getPdo()->query($sql);

        foreach ($scopeList as $scope) {
            if (!$this->getMetadata()->get(['scopes', $scope, 'entity'])) continue;
            if ($this->getMetadata()->get(['scopes', $scope, 'disabled'])) continue;

            $seed = $this->getEntityManager()->getEntity($scope);
            if (!$seed) continue;

            $attributeList = [];

            foreach ($seed->getAttributes() as $attribute => $defs) {
                if (!isset($defs['type']) || $defs['type'] !== \Espo\ORM\Entity::JSON_ARRAY) continue;
                if (!$seed->getAttributeParam($attribute, 'storeArrayValues')) continue;
                if ($seed->getAttributeParam($attribute, 'notStorable')) continue;
                $attributeList[] = $attribute;
            }
            $select = ['id'];
            $orGroup = [];
            foreach ($attributeList as $attribute) {
                $select[] = $attribute;
                $orGroup[$attribute . '!='] = null;
            }

            $sql = $this->getEntityManager()->getQuery()->createSelectQuery($scope, [
                'select' => $select,
                'whereClause' => [
                    'OR' => $orGroup
                ]
            ]);
            $sth = $this->getEntityManager()->getPdo()->prepare($sql);
            $sth->execute();

            while ($dataRow = $sth->fetch(\PDO::FETCH_ASSOC)) {
                $entity = $this->getEntityManager()->getEntityFactory()->create($scope);
                $entity->set($dataRow);
                $entity->setAsFetched();

                foreach ($attributeList as $attribute) {
                    $this->getEntityManager()->getRepository('ArrayValue')->storeEntityAttribute($entity, $attribute, true);
                }
            }
        }
    }

    // TODO remove in 5.5.0
    public function jobPopulateNotesTeamUser()
    {
        $aclManager = $this->getInjection('container')->get('aclManager');

        $sql = $this->getEntityManager()->getQuery()->createSelectQuery('Note', [
            'whereClause' => [
                'parentId!=' => null,
                'type=' => ['Relate', 'CreateRelated', 'EmailReceived', 'EmailSent', 'Assign', 'Create'],
            ],
            'limit' => 100000,
            'orderBy' => [['number', 'DESC']]
        ]);
        $sth = $this->getEntityManager()->getPdo()->prepare($sql);
        $sth->execute();

        $i = 0;
        while ($dataRow = $sth->fetch(\PDO::FETCH_ASSOC)) {
            $i++;
            $note = $this->getEntityManager()->getEntityFactory()->create('Note');
            $note->set($dataRow);
            $note->setAsFetched();

            if ($note->get('relatedId') && $note->get('relatedType')) {
                $targetType = $note->get('relatedType');
                $targetId = $note->get('relatedId');
            } else if ($note->get('parentId') && $note->get('parentType')) {
                $targetType = $note->get('parentType');
                $targetId = $note->get('parentId');
            } else {
                continue;
            }

            if (!$this->getEntityManager()->hasRepository($targetType)) continue;

            try {
                $entity = $this->getEntityManager()->getEntity($targetType, $targetId);
                if (!$entity) continue;
                $ownerUserIdAttribute = $aclManager->getImplementation($targetType)->getOwnerUserIdAttribute($entity);
                $toSave = false;
                if ($ownerUserIdAttribute) {
                    if ($entity->getAttributeParam($ownerUserIdAttribute, 'isLinkMultipleIdList')) {
                        $link = $entity->getAttributeParam($ownerUserIdAttribute, 'relation');
                        $userIdList = $entity->getLinkMultipleIdList($link);
                    } else {
                        $userId = $entity->get($ownerUserIdAttribute);
                        if ($userId) {
                            $userIdList = [$userId];
                        } else {
                            $userIdList = [];
                        }
                    }
                    if (!empty($userIdList)) {
                        $note->set('usersIds', $userIdList);
                        $toSave = true;
                    }
                }
                if ($entity->hasLinkMultipleField('teams')) {
                    $teamIdList = $entity->getLinkMultipleIdList('teams');
                    if (!empty($teamIdList)) {
                        $note->set('teamsIds', $teamIdList);
                        $toSave = true;
                    }
                }
                if ($toSave) {
                    $this->getEntityManager()->saveEntity($note);
                }
            } catch (\Exception $e) {}
        }
    }

    public function jobPopulateOptedOutPhoneNumbers()
    {
        $entityTypeList = ['Contact', 'Lead'];

        foreach ($entityTypeList as $entityType) {
            $entityList = $this->getEntityManager()->getRepository($entityType)->where([
                'doNotCall' => true,
                'phoneNumber!=' => null,
            ])->select(['id', 'phoneNumber'])->find();
            foreach ($entityList as $entity) {
                $phoneNumber = $entity->get('phoneNumber');
                if (!$phoneNumber) continue;
                $phoneNumberEntity = $this->getEntityManager()->getRepository('PhoneNumber')->getByNumber($phoneNumber);
                if (!$phoneNumberEntity) continue;
                $phoneNumberEntity->set('optOut', true);
                $this->getEntityManager()->saveEntity($phoneNumberEntity);

            }
        }
    }
}
