/home/ivoiecob/email.hirewise-va.com/modules/SharedContacts/Module.php
<?php
/**
 * This code is licensed under Afterlogic Software License.
 * For full statements of the license see LICENSE file.
 */

namespace Aurora\Modules\SharedContacts;

use Afterlogic\DAV\Backend;
use Afterlogic\DAV\Constants;
use Aurora\Api;
use Aurora\Modules\Contacts\Enums\Access;
use Aurora\Modules\Contacts\Enums\SortField;
use Aurora\Modules\Contacts\Enums\StorageType;
use Aurora\Modules\Contacts\Classes\Contact;
use Aurora\Modules\Contacts\Module as ContactsModule;
use Aurora\Modules\Core\Models\User;
use Aurora\System\Enums\UserRole;
use Aurora\System\Exceptions\InvalidArgumentException;
use Aurora\System\Notifications;
use Illuminate\Database\Capsule\Manager as Capsule;
use Sabre\DAV\UUIDUtil;
use Aurora\Modules\Core\Module as CoreModule;

/**
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2023, Afterlogic Corp.
 *
 * @property Settings $oModuleSettings
 *
 * @package Modules
 */
class Module extends \Aurora\System\Module\AbstractModule
{
    protected static $iStorageOrder = 10;

    protected $oBeforeDeleteUser = null;

    protected $storagesMapToAddressbooks = [
        StorageType::Shared => Constants::ADDRESSBOOK_SHARED_WITH_ALL_NAME
    ];

    public function init()
    {
        $this->subscribeEvent('Contacts::PrepareFiltersFromStorage', array($this, 'onPrepareFiltersFromStorage'));
        $this->subscribeEvent('Contacts::UpdateSharedContacts::after', array($this, 'onAfterUpdateSharedContacts'));

        $this->subscribeEvent('Contacts::CheckAccessToObject::after', array($this, 'onAfterCheckAccessToObject'));
        $this->subscribeEvent('Contacts::GetContactSuggestions', array($this, 'onGetContactSuggestions'));
        $this->subscribeEvent('Contacts::GetAddressBooks::after', array($this, 'onAfterGetAddressBooks'), 1000);
        // $this->subscribeEvent('Contacts::PopulateContactModel', array($this, 'onPopulateContactModel'));

        $this->subscribeEvent('Contacts::ContactQueryBuilder', array($this, 'onContactQueryBuilder'));
        $this->subscribeEvent('Contacts::CreateContact::before', array($this, 'onBeforeCreateContact'));
        $this->subscribeEvent('Contacts::CheckAccessToAddressBook::after', array($this, 'onAfterCheckAccessToAddressBook'));

        $this->subscribeEvent(self::GetName() . '::UpdateAddressbookShare::before', array($this, 'onBeforeUpdateAddressbookShare'));
        $this->subscribeEvent(self::GetName() . '::GetSharesForAddressbook::before', array($this, 'onBeforeUpdateAddressbookShare'));
        $this->subscribeEvent(self::GetName() . '::LeaveShare::before', array($this, 'onBeforeUpdateAddressbookShare'));

        $this->subscribeEvent('Contacts::GetContacts::before', array($this, 'populateContactArguments'));
        $this->subscribeEvent('Contacts::GetContactsByEmails::before', array($this, 'populateContactArguments'));
        $this->subscribeEvent('Contacts::GetContacts::after', array($this, 'onGetContacts'));
        $this->subscribeEvent('Contacts::GetContactsByUids::after', array($this, 'onGetContactsByUids'));
        $this->subscribeEvent('Contacts::PopulateContactArguments', array($this, 'populateContactArguments'));
        $this->subscribeEvent('Contacts::DeleteContacts::before', array($this, 'onBeforeDeleteContacts'));
        $this->subscribeEvent('Contacts::GetStoragesMapToAddressbooks::after', array($this, 'onAfterGetStoragesMapToAddressbooks'));
        $this->subscribeEvent('Contacts::Export::before', array($this, 'populateContactArguments'));

        $this->subscribeEvent('Core::AddUsersToGroup::after', [$this, 'onAfterAddUsersToGroup']);
        $this->subscribeEvent('Core::RemoveUsersFromGroup::after', [$this, 'onAfterRemoveUsersFromGroup']);
        $this->subscribeEvent('Core::CreateUser::after', [$this, 'onAfterCreateUser']);
        $this->subscribeEvent('Core::UpdateUser::after', [$this, 'onAfterUpdateUser']);
        $this->subscribeEvent('Core::DeleteUser::before', [$this, 'onBeforeDeleteUser']);
        $this->subscribeEvent('Core::DeleteUser::after', [$this, 'onAfterDeleteUser']);
        $this->subscribeEvent('Core::DeleteGroup::after', [$this, 'onAfterDeleteGroup']);
    }

    /**
     * @return Module
     */
    public static function getInstance()
    {
        return parent::getInstance();
    }

    /**
     * @return Module
     */
    public static function Decorator()
    {
        return parent::Decorator();
    }

    /**
     * @return Settings
     */
    public function getModuleSettings()
    {
        return $this->oModuleSettings;
    }

    public function GetAddressbooks($UserId)
    {
        $mResult = [];

        Api::checkUserRoleIsAtLeast(UserRole::NormalUser);
        Api::CheckAccess($UserId);

        $dBPrefix = Api::GetSettings()->DBPrefix;
        $stmt = Api::GetPDO()->prepare("
		select ab.*, sab.access, sab.group_id, sab.addressbookuri from " . $dBPrefix . "adav_shared_addressbooks sab 
		left join " . $dBPrefix . "adav_addressbooks ab on sab.addressbook_id = ab.id
			where sab.principaluri = ?
		");

        $principalUri = Constants::PRINCIPALS_PREFIX . Api::getUserPublicIdById($UserId);
        $stmt->execute([
            $principalUri
        ]);

        $abooks = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        foreach ($abooks as $abook) {
            if ($abook['principaluri'] !== $principalUri) {
                if (isset($abook['id'])) {
                    $storage =  StorageType::Shared . '-' . $abook['id'];
                } else {
                    $storage =  StorageType::Shared . '-' . StorageType::Personal;
                }

                if (count($mResult) > 0) {
                    foreach ($mResult as $key => $val) {
                        if ($val['Id'] === $storage) {
                            $iAccess = $mResult[$key]['Access'];
                            if ($val['GroupId'] === 0 && $iAccess === Access::NoAccess) {
                                continue 2;
                            }
                            $iNewAccess = $abook['access'];
                            if ((int) $abook['group_id'] === 0) { //personal sharing
                                $mResult[$key]['Access'] = $iNewAccess;
                            } else { // group sharing
                                if ($iNewAccess !== Access::Read) {
                                    if ($iAccess < $iNewAccess || $iNewAccess === Access::NoAccess) {
                                        $mResult[$key]['Access'] = $iNewAccess;
                                    }
                                } elseif ($iAccess !== Access::Write) {
                                    $mResult[$key]['Access'] = $iNewAccess;
                                }
                            }
                            continue 2;
                        }
                    }
                }

                $prevState = Api::skipCheckUserRole(true);
                Api::skipCheckUserRole($prevState);

                $mResult[] = [
                    'Id' => $storage,
                    'EntityId' => isset($abook['id']) ? (int) $abook['id'] : null,
                    'CTag' => isset($abook['synctoken']) ? (int) $abook['synctoken'] : 0,
                    'Display' => true,
                    'Order' => 1,
                    'DisplayName' => $abook['displayname'] . ' (' . basename($abook['principaluri']) . ')',
                    'Uri' => $abook['addressbookuri'],
                    'Url' => 'addressbooks/' . $abook['addressbookuri'],
                    'Shared' => true,
                    'Access' => (int) $abook['access'],
                    'Owner' => basename($abook['principaluri']),
                    'GroupId' => (int) $abook['group_id']
                ];
            }
        }

        return array_filter($mResult, function ($item) {
            return ($item['Access'] !== Access::NoAccess);
        });
    }

    public function GetSharesForAddressbook($UserId, $Id)
    {
        Api::checkUserRoleIsAtLeast(UserRole::NormalUser);
        Api::CheckAccess($UserId);

        $aResult = [];

        $shares = $this->_getSharesForAddressbook($UserId, $Id);
        if (count($shares) > 0) {
            $oUser = Api::getUserById($UserId);
            $groups = [];
            foreach ($shares as $share) {
                if ($share['group_id'] != 0) {
                    if (!in_array($share['group_id'], $groups)) {
                        $oGroup = CoreModule::Decorator()->GetGroup((int) $share['group_id']);
                        if ($oGroup) {
                            $groups[] = $share['group_id'];
                            $aResult[] = [
                                'PublicId' => $oGroup->getName(),
                                'Access' => (int) $share['access'],
                                'IsGroup' => true,
                                'IsAll' => !!$oGroup->IsAll,
                                'GroupId' => (int) $share['group_id']
                            ];
                        }
                    }
                } else {
                    $aResult[] = [
                        'PublicId' => basename($share['principaluri']),
                        'Access' => (int) $share['access']
                    ];
                }
            }
        }

        return $aResult;
    }

    protected function _getSharesForAddressbook($iUserId, $abookComplexId)
    {
        $dBPrefix = Api::GetSettings()->DBPrefix;
        $stmt = Api::GetPDO()->prepare("
		select sab.* from " . $dBPrefix . "adav_shared_addressbooks sab 
		left join " . $dBPrefix . "adav_addressbooks ab on sab.addressbook_id = ab.id
			left join " . $dBPrefix . "core_users cu on ab.principaluri = CONCAT('principals/', cu.PublicId)
				where cu.Id = ? AND ab.id = ?
		");

        $stmt->execute([
            $iUserId,
            $abookComplexId
        ]);

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    protected function getShareForAddressbook($iUserId, $abookComplexId, $principalUri, $groupId = 0)
    {
        $dBPrefix = Api::GetSettings()->DBPrefix;
        $stmt = Api::GetPDO()->prepare("
		select sab.*
		from " . $dBPrefix . "adav_shared_addressbooks sab 
		left join " . $dBPrefix . "adav_addressbooks ab on sab.addressbook_id = ab.id
			left join " . $dBPrefix . "core_users cu on ab.principaluri = CONCAT('principals/', cu.PublicId)
				where cu.Id = ? and sab.principaluri = ? and sab.group_id = ? and ab.id = ?
		");

        $stmt->execute([
            $iUserId,
            $principalUri,
            $groupId,
            $abookComplexId
        ]);

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    protected function deleteShareByPublicIds($userId, $abookComplexId, $publicIds)
    {
        $dBPrefix = Api::GetSettings()->DBPrefix;

        $sharesIds = [];
        foreach ($publicIds as $publicId) {
            $publicId = \json_decode($publicId);
            $principaluri = $publicId[0] ? Constants::PRINCIPALS_PREFIX . $publicId[0] : '';
            $shares = $this->getShareForAddressbook($userId, $abookComplexId, $principaluri, $publicId[1]);
            if (is_array($shares) && count($shares) > 0) {
                $ids = array_map(function ($share) {
                    return $share['id'];
                }, $shares);
                $sharesIds = array_merge($sharesIds, $ids);
            }
        }
        if (count($sharesIds) > 0) {
            $stmt = Api::GetPDO()->prepare("delete from " . $dBPrefix . "adav_shared_addressbooks where id in (" . \implode(',', $sharesIds) . ")");
            $stmt->execute();
        }
    }

    protected function getAddressbook($iUserId, $abookId)
    {
        $mResult = false;

        $dBPrefix = Api::GetSettings()->DBPrefix;

        $userPublicId = Api::getUserPublicIdById($iUserId);
        if (!empty($abookId) && $userPublicId) {
            $stmt = Api::GetPDO()->prepare("select * from " . $dBPrefix . "adav_addressbooks where principaluri = ? and id = ?");
            $stmt->execute([Constants::PRINCIPALS_PREFIX . $userPublicId, $abookId]);
            $mResult = $stmt->fetch(\PDO::FETCH_ASSOC);
        }

        return $mResult;
    }

    protected function createShare($iUserId, $abookComplexId, $share)
    {
        $dBPrefix = Api::GetSettings()->DBPrefix;

        $book = $this->getAddressbook($iUserId, $abookComplexId);
        if ($book) {
            $shareePublicId = $share['PublicId'];
            $access = $share['Access'];
            $groupId = $share['GroupId'];
            $stmt = Api::GetPDO()->prepare("insert into " . $dBPrefix . "adav_shared_addressbooks
			(principaluri, access, addressbook_id, addressbookuri, group_id)
			values (?, ?, ?, ?, ?)");
            $principaluri = $shareePublicId ? Constants::PRINCIPALS_PREFIX . $shareePublicId : '';
            $stmt->execute([$principaluri, $access, $book['id'], UUIDUtil::getUUID(), $groupId]);
        }
    }

    protected function updateShare($iUserId, $abookComplexId, $share)
    {
        $dBPrefix = Api::GetSettings()->DBPrefix;
        $book = $this->getAddressbook($iUserId, $abookComplexId);
        if ($book) {
            $shareePublicId = $share['PublicId'];
            $access = $share['Access'];
            $groupId = $share['GroupId'];
            $stmt = Api::GetPDO()->prepare("update " . $dBPrefix . "adav_shared_addressbooks 
			set access = ? where principaluri = ? and addressbook_id = ? and group_id = ?");
            $stmt->execute([$access, Constants::PRINCIPALS_PREFIX . $shareePublicId, $book['id'], $groupId]);
        }
    }

    public function onPrepareFiltersFromStorage(&$aArgs, &$mResult)
    {
        if (isset($aArgs['Storage']) && ($aArgs['Storage'] === StorageType::Shared || $aArgs['Storage'] === StorageType::All)) {
            $oUser = Api::getUserById($aArgs['UserId']);
            $aArgs['IsValid'] = true;

            $query = Capsule::connection()->table('adav_shared_addressbooks')
                ->select('addressbook_id')
                ->from('adav_shared_addressbooks')
                ->leftJoin('adav_addressbooks', 'adav_shared_addressbooks.addressbook_id', '=', 'adav_addressbooks.id')
                ->where('adav_addressbooks.principaluri', '<>', Constants::PRINCIPALS_PREFIX . $oUser->PublicId)
                ->where('adav_shared_addressbooks.principaluri', Constants::PRINCIPALS_PREFIX . $oUser->PublicId);

            if ($aArgs['Storage'] === StorageType::Shared && isset($aArgs['AddressBookId'])) {
                $query->where('addressbook_id', (int) $aArgs['AddressBookId']);
            }
            $ids = $query->pluck('addressbook_id')->all();

            if ($aArgs['Storage'] === StorageType::All && empty($aArgs['AddressBookId'])) {
                $addressbook = $this->GetSharedWithAllAddressbook($oUser->Id);
                if ($addressbook) {
                    $ids[] = (int) $addressbook['id'];
                }
            }

            if ($ids) {
                $mResult->whereIn('adav_cards.addressbookid', $ids, 'or');
            }

            if (isset($aArgs['Query'])) {
                if ($ids) {
                    $aArgs['Query']->addSelect(Capsule::connection()->raw(
                        '
                    CASE
                        WHEN ' . Capsule::connection()->getTablePrefix() . 'adav_cards.addressbookid IN (' . implode(',', $ids) . ') THEN true
                        ELSE false
                    END as Shared'
                    ));
                } else {
                    $aArgs['Query']->addSelect(Capsule::connection()->raw(
                        'false as Shared'
                    ));
                }
            }
        }
    }

    public function onAfterUpdateSharedContacts($aArgs, &$mResult)
    {
        $oContacts = ContactsModule::Decorator();
        $aUUIDs = isset($aArgs['UUIDs']) ? $aArgs['UUIDs'] : [];

        foreach ($aUUIDs as $sUUID) {
            $oContact = $oContacts->GetContact($sUUID, $aArgs['UserId']);
            if ($oContact instanceof Contact) {
                $FromStorage = $oContact->Storage;
                $ToStorage = $FromStorage;
                if ($oContact->Storage === StorageType::Shared) {
                    $ToStorage = StorageType::Personal;
                } elseif ($oContact->Storage === StorageType::Personal) {
                    $ToStorage = StorageType::Shared;
                }
                $oContacts->MoveContactsToStorage($aArgs['UserId'], $FromStorage, $ToStorage, [$sUUID]);
            }
        }
    }

    public function onAfterCheckAccessToObject(&$aArgs, &$mResult)
    {
        $oUser = $aArgs['User'];
        $oContact = isset($aArgs['Contact']) ? $aArgs['Contact'] : null;
        $Access = isset($aArgs['Access']) ? (int) $aArgs['Access'] : null;

        if ($oContact instanceof Contact) {
            if ($oContact->IdUser === $oUser->Id) {
                $mResult = true;
                return true; // break other subscriptions
            }
            if ($oContact->Storage === StorageType::Shared) {
                $sharedWithAllAddressBook = self::Decorator()->GetSharedWithAllAddressbook($oUser->Id);
                if ($oUser->Role === UserRole::SuperAdmin || ($sharedWithAllAddressBook && $oContact->AddressBookId == $sharedWithAllAddressBook['id'])) {
                    $mResult = true;
                } else {
                    $mResult = false;
                }
                return true; // break other subscriptions
            } elseif ($oContact->Storage === StorageType::Personal || $oContact->Storage === StorageType::AddressBook) {
                $dBPrefix = Api::GetSettings()->DBPrefix;
                $sql = "select ab.*, sab.access, ab.id as addressbook_id, cu.Id as UserId from " . $dBPrefix . "adav_shared_addressbooks sab 
				left join " . $dBPrefix . "adav_addressbooks ab on sab.addressbook_id = ab.id
					left join " . $dBPrefix . "core_users cu on ab.principaluri = CONCAT('principals/', cu.PublicId)
						where sab.principaluri = ? and cu.Id = ? and ab.id = ?";

                $stmt = Api::GetPDO()->prepare($sql);

                $stmt->execute([
                    Constants::PRINCIPALS_PREFIX . $oUser->PublicId,
                    $oContact->IdUser,
                    $oContact->AddressBookId
                ]);

                $abook = $stmt->fetch(\PDO::FETCH_ASSOC);
                if ($abook) {
                    if ((int) $abook['access'] === Access::NoAccess) {
                        $mResult = false;
                    } elseif (isset($Access)) {
                        if ($Access === (int) $abook['access'] && $Access === Access::Write) {
                            $mResult = true;
                        } else {
                            $mResult = false;
                        }
                    } else {
                        $mResult = true;
                    }

                    return true; // break other subscriptions
                }
            }
        }
    }

    public function onGetContactSuggestions(&$aArgs, &$mResult)
    {
        if ($aArgs['Storage'] === 'all' || $aArgs['Storage'] === StorageType::Shared) {
            $mResult[StorageType::Shared] = ContactsModule::Decorator()->GetContacts(
                $aArgs['UserId'],
                StorageType::Shared,
                0,
                $aArgs['Limit'],
                $aArgs['SortField'],
                $aArgs['SortOrder'],
                $aArgs['Search']
            );
        }
    }

    /**
     * The methods corrects the storage type of contacts related to the shared addressbooks.
     */
    public function onGetContacts(&$aArgs, &$mResult)
    {
        if ($aArgs['Storage'] === 'all' || $aArgs['Storage'] === StorageType::Shared) {
            $aSharedBooks = $this->GetAddressbooks($aArgs['UserId']);
            foreach ($mResult['List'] as &$aContact) {
                $aStorageParts = explode('-', $aContact['Storage']);
                // only personal and custom addressbooks can be shared
                if (in_array($aStorageParts[0], [ StorageType::Personal,StorageType::AddressBook ])) {
                    foreach ($aSharedBooks as $aBook) {
                        // check if contact's addressbook exist in list of shared with user addressbooks
                        if ($aBook['EntityId'] === $aContact['AddressBookId']) {
                            $aContact['Storage'] = StorageType::Shared . '-' . $aContact['AddressBookId'];
                        }
                    }
                }
            }
        }
    }

    /**
     * The methods corrects the storage type of contacts related to the shared addressbooks.
     */
    public function onGetContactsByUids(&$aArgs, &$mResult)
    {
        if (is_array($mResult)) {
            $aSharedBooks = $this->GetAddressbooks($aArgs['UserId']);
            foreach ($mResult as &$oContact) {
                $aStorageParts = explode('-', $oContact->Storage);
                // only personal and custom addressbooks can be shared
                if (in_array($aStorageParts[0], [ StorageType::Personal, StorageType::AddressBook ])) {
                    foreach ($aSharedBooks as $aBook) {
                        // check if contact's addressbook exist in list of shared with user addressbooks
                        if ($aBook['EntityId'] === $oContact->AddressBookId) {
                            $oContact->Storage = StorageType::Shared . '-' . $oContact->AddressBookId;
                        }
                    }
                }
            }
        }
    }

    public function GetSharedWithAllAddressbook($UserId)
    {
        Api::CheckAccess($UserId);

        $addressbook = false;

        $oUser = Api::getUserById($UserId);
        if ($oUser) {
            $sPrincipalUri = Constants::PRINCIPALS_PREFIX . $oUser->IdTenant . '_' . Constants::DAV_TENANT_PRINCIPAL;
            $addressbook = Backend::Carddav()->getAddressBookForUser($sPrincipalUri, Constants::ADDRESSBOOK_SHARED_WITH_ALL_NAME);
            if (!$addressbook) {
                if (Backend::Carddav()->createAddressBook($sPrincipalUri, Constants::ADDRESSBOOK_SHARED_WITH_ALL_NAME, ['{DAV:}displayname' => Constants::ADDRESSBOOK_SHARED_WITH_ALL_DISPLAY_NAME])) {
                    $addressbook = Backend::Carddav()->getAddressBookForUser($sPrincipalUri, Constants::ADDRESSBOOK_SHARED_WITH_ALL_NAME);
                }
            }
        }

        return $addressbook;
    }

    public function onAfterGetAddressBooks(&$aArgs, &$mResult)
    {
        if (!is_array($mResult)) {
            $mResult = [];
        }
        foreach ($mResult as $key => $abook) {
            $mResult[$key]['Shares'] = self::Decorator()->GetSharesForAddressbook($aArgs['UserId'], $abook['Id']);
        }
        $mResult = array_merge(
            $mResult,
            $this->GetAddressbooks($aArgs['UserId'])
        );

        $addressbook = $this->GetSharedWithAllAddressbook($aArgs['UserId']);
        if ($addressbook) {
            /**
             * @var array $addressbook
             */
            $mResult[] = [
                'Id' => 'shared',
                'EntityId' => (int) $addressbook['id'],
                'CTag' => (int) $addressbook['{http://sabredav.org/ns}sync-token'],
                'Display' => true,
                'Order' => 1,
                'DisplayName' => $addressbook['{DAV:}displayname'],
                'Uri' => $addressbook['uri'],
                'Url' => 'addressbooks/' . $addressbook['uri'],
            ];
        }
    }

    // @todo: it doesn't work because addressbook id never has 3 parts
    // public function onPopulateContactModel(&$oContact, &$mResult)
    // {
    //     if ($oContact instanceof Contact) {
    //         $aStorageParts = \explode('-', $oContact->Storage);
    //         if (is_array($aStorageParts) && count($aStorageParts) === 3 && $aStorageParts[0] === StorageType::Shared) {
    //             $abooks = $this->GetAddressbooks($oContact->IdUser);
    //             foreach ($abooks as $abook) {
    //                 if ($abook['Id'] === $oContact->Storage) {
    //                     if ($aStorageParts[2] === StorageType::Personal) {
    //                         $oContact->Storage = StorageType::Personal;
    //                     } else {
    //                         $oContact->Storage = StorageType::AddressBook;
    //                         $oContact->AddressBookId = (int) $aStorageParts[2];
    //                     }

    //                     $oContact->IdUser = (int) $aStorageParts[1];
    //                     break;
    //                 }
    //             }
    //         }
    //     }
    // }

    public function UpdateAddressbookShare($UserId, $Id, $Shares)
    {
        $mResult = true;

        Api::checkUserRoleIsAtLeast(UserRole::NormalUser);
        Api::CheckAccess($UserId);

        if (!isset($Id) || !is_array($Shares)) {
            throw new InvalidArgumentException("", Notifications::InvalidInputParameter);
        }

        try {
            $oUser = Api::getUserById($UserId);
            $currentABookShares = $this->_getSharesForAddressbook($UserId, $Id);

            $newABookShares = [];
            foreach ($Shares as $share) {
                if (isset($share['GroupId'])) {
                    $aUsers = CoreModule::Decorator()->GetGroupUsers($oUser->IdTenant, (int) $share['GroupId']);
                    $newABookShares[] = [
                        'PublicId' => '',
                        'Access' => (int) $share['Access'],
                        'GroupId' => (int) $share['GroupId'],
                    ];
                    foreach ($aUsers as $aUser) {
                        $newABookShares[] = [
                            'PublicId' => $aUser['PublicId'],
                            'Access' => (int) $share['Access'],
                            'GroupId' => (int) $share['GroupId'],
                        ];
                    }
                } else {
                    $share['GroupId'] = 0;
                    $newABookShares[] = $share;
                }
            }

            $currentShares = array_map(function ($share) {
                return \json_encode([
                    basename($share['principaluri']),
                    $share['group_id']
                ]);
            }, $currentABookShares);

            $newShares = array_map(function ($share) {
                return \json_encode([
                    $share['PublicId'],
                    $share['GroupId']
                ]);
            }, $newABookShares);

            $sharesToDelete = array_diff($currentShares, $newShares);
            $sharesToCreate = array_diff($newShares, $currentShares);
            $sharesToUpdate = array_intersect($currentShares, $newShares);

            if (count($sharesToDelete) > 0) {
                $this->deleteShareByPublicIds($UserId, $Id, $sharesToDelete);
            }

            foreach ($newABookShares as $share) {
                $sharePublicIdAndGroupId = \json_encode([
                    $share['PublicId'],
                    $share['GroupId']
                ]);
                if (in_array($sharePublicIdAndGroupId, $sharesToCreate)) {
                    $this->createShare($UserId, $Id, $share);
                }
                if (in_array($sharePublicIdAndGroupId, $sharesToUpdate)) {
                    $this->updateShare($UserId, $Id, $share);
                }
            }

            $mResult = true;
        } catch (\Exception $oException) {
            Api::LogException($oException);
            $mResult = false;
        }

        return $mResult;
    }

    public function LeaveShare($UserId, $Id)
    {
        Api::checkUserRoleIsAtLeast(UserRole::NormalUser);
        Api::CheckAccess($UserId);

        $userPublicId = Api::getUserPublicIdById($UserId);

        $sharedAddressBook =  Capsule::connection()->table('adav_shared_addressbooks')
            ->where('principaluri', Constants::PRINCIPALS_PREFIX . $userPublicId)
            ->where('addressbook_id', $Id)
            ->where('group_id', 0)
            ->first();

        if ($sharedAddressBook) {
            Capsule::connection()->table('adav_shared_addressbooks')
            ->where('principaluri', Constants::PRINCIPALS_PREFIX . $userPublicId)
            ->where('addressbook_id', $Id)
            ->where('group_id', 0)
            ->update(['access' => Access::NoAccess]);
        } else {
            $stmt = Api::GetPDO()->prepare("insert into " . Api::GetSettings()->DBPrefix . "adav_shared_addressbooks
			(principaluri, access, addressbook_id, addressbookuri, group_id)
			values (?, ?, ?, ?, ?)");
            $stmt->execute([Constants::PRINCIPALS_PREFIX . $userPublicId,  Access::NoAccess, $Id, UUIDUtil::getUUID(), 0]);
        }

        return true;
    }

    public function onAfterDeleteGroup($aArgs, &$mResult)
    {
        if ($mResult) {
            $dBPrefix = Api::GetSettings()->DBPrefix;
            $stmt = Api::GetPDO()->prepare("delete from " . $dBPrefix . "adav_shared_addressbooks where group_id = ?");
            $stmt->execute([$aArgs['GroupId']]);
        }
    }

    /**
     * @ignore
     * @param array $aArgs Arguments of event.
     * @param mixed $mResult Is passed by reference.
     */
    public function onBeforeDeleteUser($aArgs, &$mResult)
    {
        if (isset($aArgs['UserId'])) {
            $this->oBeforeDeleteUser = Api::getUserById($aArgs['UserId']);
        }
    }

    /**
     * @ignore
     * @param array $aArgs Arguments of event.
     * @param mixed $mResult Is passed by reference.
     */
    public function onAfterDeleteUser($aArgs, $mResult)
    {
        if ($mResult && $this->oBeforeDeleteUser instanceof User) {
            $dBPrefix = Api::GetSettings()->DBPrefix;
            $stmt = Api::GetPDO()->prepare("delete from " . $dBPrefix . "adav_shared_addressbooks where principaluri = ?");
            $stmt->execute([Constants::PRINCIPALS_PREFIX . $this->oBeforeDeleteUser->PublicId]);
        }
    }

    public function onAfterAddUsersToGroup($aArgs, &$mResult)
    {
        if ($mResult) {
            foreach ($aArgs['UserIds'] as $iUserId) {
                $userPublicId = Api::getUserPublicIdById($iUserId);
                $sUserPrincipalUri = Constants::PRINCIPALS_PREFIX . $userPublicId;

                $dBPrefix = Api::GetSettings()->DBPrefix;
                $stmt = Api::GetPDO()->prepare("select distinct addressbook_id, access from " . $dBPrefix . "adav_shared_addressbooks where group_id = ?");
                $stmt->execute([$aArgs['GroupId']]);
                $shares = $stmt->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($shares as $share) {
                    if (is_array($share)) {
                        $stmt = Api::GetPDO()->prepare("insert into " . $dBPrefix . "adav_shared_addressbooks
						(principaluri, access, addressbook_id, addressbookuri, group_id)
						values (?, ?, ?, ?, ?)");
                        $stmt->execute([$sUserPrincipalUri, $share['access'], $share['addressbook_id'], UUIDUtil::getUUID(), $aArgs['GroupId']]);
                    }
                }
            }
        }
    }

    public function onAfterCreateUser($aArgs, &$mResult)
    {
        if ($mResult) {
            $oUser = User::find($mResult);
            if ($oUser) {
                $oGroup = CoreModule::getInstance()->GetAllGroup($oUser->IdTenant);
                if ($oGroup) {
                    $newArgs = [
                        'GroupId' => $oGroup->Id,
                        'UserIds' => [$mResult]
                    ];
                    $newResult = true;
                    $this->onAfterAddUsersToGroup($newArgs, $newResult);
                }
            }
        }
    }

    public function onAfterUpdateUser($aArgs, &$mResult)
    {
        if ($mResult) {
            $groupIds = $aArgs['GroupIds'] ?? null;
            $userId = $aArgs['UserId'];

            if ($groupIds !== null) {
                $userPublicId = Api::getUserPublicIdById($userId);
                $sUserPrincipalUri = Constants::PRINCIPALS_PREFIX . $userPublicId;

                $dBPrefix = Api::GetSettings()->DBPrefix;
                $stmt = Api::GetPDO()->prepare("select * from " . $dBPrefix . "adav_shared_addressbooks where group_id <> 0 and principaluri = ?");
                $stmt->execute([$sUserPrincipalUri]);
                $shares = $stmt->fetchAll(\PDO::FETCH_ASSOC);

                $currentGroupsIds = [];
                if (is_array($shares)) {
                    $currentGroupsIds = array_map(function ($share) {
                        return $share['group_id'];
                    }, $shares);
                }

                $groupsIdsToDelete = array_diff($currentGroupsIds, $groupIds);
                $groupsIdsToCreate = array_diff($groupIds, $currentGroupsIds);

                if (count($groupsIdsToDelete) > 0) {
                    $stmt = Api::GetPDO()->prepare("delete from " . $dBPrefix . "adav_shared_addressbooks 
					where group_id in (" . \implode(',', $groupsIdsToDelete) . ") and principaluri = ?");
                    $stmt->execute([$sUserPrincipalUri]);
                }

                if (count($groupsIdsToCreate) > 0) {
                    $stmt = Api::GetPDO()->prepare("select distinct addressbook_id, access, group_id from " . $dBPrefix . "adav_shared_addressbooks where group_id in (" . \implode(',', $groupsIdsToCreate) . ")");
                    $stmt->execute();
                    $shares = $stmt->fetchAll(\PDO::FETCH_ASSOC);
                    foreach ($shares as $share) {
                        if (is_array($share)) {
                            $stmt = Api::GetPDO()->prepare("insert into " . $dBPrefix . "adav_shared_addressbooks
							(principaluri, access, addressbook_id, addressbookuri, group_id)
							values (?, ?, ?, ?, ?)");
                            $stmt->execute([$sUserPrincipalUri, $share['access'], $share['addressbook_id'], UUIDUtil::getUUID(), $share['group_id']]);
                        }
                    }
                }
            }
        }
    }

    public function onAfterRemoveUsersFromGroup($aArgs, &$mResult)
    {
        if ($mResult) {
            $principals = [];
            foreach ($aArgs['UserIds'] as $iUserId) {
                $oUser = Api::getUserById($iUserId);
                $principals[] = Constants::PRINCIPALS_PREFIX . $oUser->PublicId;
            }

            if (count($principals) > 0) {
                $dBPrefix = Api::GetSettings()->DBPrefix;
                $principals = array_map(function ($principal) {
                    return \Illuminate\Support\Str::padBoth($principal, strlen($principal) + 2, "'");
                }, $principals);
                $stmt = Api::GetPDO()->prepare("delete from " . $dBPrefix . "adav_shared_addressbooks where principaluri in (" . \implode(',', $principals) . ") and group_id = ?");
                $stmt->execute([$aArgs['GroupId']]);
            }
        }
    }

    public function onContactQueryBuilder(&$aArgs, &$query)
    {
        $userPublicId = Api::getUserPublicIdById($aArgs['UserId']);

        if (isset($aArgs['Query'])) {
            $aArgs['Query']->leftJoin('adav_shared_addressbooks', 'adav_cards.addressbookid', '=', 'adav_shared_addressbooks.addressbook_id');
        }
        $query->orWhere(function ($q) use ($userPublicId, $aArgs) {
            $q->where('adav_shared_addressbooks.principaluri', Constants::PRINCIPALS_PREFIX . $userPublicId);
            if (is_array($aArgs['UUID'])) {
                $ids = $aArgs['UUID'];
                if (count($aArgs['UUID']) === 0) {
                    $ids = [null];
                }
                $q->whereIn('adav_cards.id', $ids);
            } else {
                $q->where('adav_cards.id', $aArgs['UUID']);
            }
            $q->where('adav_shared_addressbooks.access', '<>', 0);
        });

        $addressbook = $this->GetSharedWithAllAddressbook($aArgs['UserId']);
        $query->orWhere(function ($q) use ($addressbook, $aArgs) {
            $q->where('adav_addressbooks.id', $addressbook['id']);
            if (is_array($aArgs['UUID'])) {
                if (count($aArgs['UUID']) > 0) {
                    $q->whereIn('adav_cards.id', $aArgs['UUID']);
                }
            } else {
                $q->where('adav_cards.id', $aArgs['UUID']);
            }
        });
    }

    public function onBeforeCreateContact(&$aArgs, &$mResult)
    {
        if (isset($aArgs['Contact'])) {
            if (isset($aArgs['UserId'])) {
                $aArgs['Contact']['UserId'] = $aArgs['UserId'];
            }
            $this->populateContactArguments($aArgs['Contact'], $mResult);
        }
    }

    /**
     *
     */
    public function populateContactArguments(&$aArgs, &$mResult)
    {
        if (isset($aArgs['Storage'], $aArgs['UserId'])) {
            $aStorageParts = \explode('-', $aArgs['Storage']);
            if (count($aStorageParts) > 1) {
                $iAddressBookId = $aStorageParts[1];
                if ($aStorageParts[0] === StorageType::Shared) {
                    if (!is_numeric($iAddressBookId)) {
                        return;
                    }
                    $aArgs['Storage'] = $aStorageParts[0];
                    $aArgs['AddressBookId'] = $iAddressBookId;

                    $mResult = true;
                }
            } else {
                if ($aStorageParts[0] === StorageType::Shared) {
                    $abook = $this->GetSharedWithAllAddressbook($aArgs['UserId']);
                    if ($abook) {
                        $aArgs['AddressBookId'] = $abook['id'];

                        $mResult = true;
                    }
                }
            }
        }
    }

    public function onBeforeDeleteContacts(&$aArgs, &$mResult)
    {
        $this->populateContactArguments($aArgs, $mResult);
        if (isset($aArgs['AddressBookId'])) {
            $aArgs['Storage'] = $aArgs['AddressBookId'];
        }
    }

    public function onAfterCheckAccessToAddressBook(&$aArgs, &$mResult)
    {
        if (isset($aArgs['User'], $aArgs['AddressBookId'])) {
            $query = Capsule::connection()->table('adav_addressbooks')
                ->select('adav_addressbooks.id', 'adav_shared_addressbooks.access', 'adav_shared_addressbooks.group_id')
                ->leftJoin('adav_shared_addressbooks', 'adav_addressbooks.id', '=', 'adav_shared_addressbooks.addressbook_id')
                ->where('adav_shared_addressbooks.principaluri', Constants::PRINCIPALS_PREFIX . $aArgs['User']->PublicId)
                ->where('adav_addressbooks.id', $aArgs['AddressBookId']);
            $abooks = $query->get();

            $val = null;
            foreach ($abooks as $abook) {
                if ($val) {
                    $iAccess = $val['Access'];
                    if ($val['GroupId'] === 0 && $iAccess === Access::NoAccess) {
                        continue;
                    }
                    $iNewAccess = $abook->access;
                    if ((int) $abook->group_id === 0) { //personal sharing
                        $val['Access'] = $iNewAccess;
                    } else { // group sharing
                        if ($iNewAccess !== Access::Read) {
                            if ($iAccess < $iNewAccess || $iNewAccess === Access::NoAccess) {
                                $val['Access'] = $iNewAccess;
                            }
                        } elseif ($iAccess !== Access::Write) {
                            $val['Access'] = $iNewAccess;
                        }
                    }
                }

                $val = [
                    'Access' => (int) $abook->access,
                    'GroupId' => (int) $abook->group_id
                ];
            }
            if ($val) {
                $access = $val['Access'];
                $mResult = ($access === Access::Write || $access === $aArgs['Access']);

                return true;
            } else {
                if (isset($aArgs['User'], $aArgs['AddressBookId'])) {
                    $addressbook = $this->GetSharedWithAllAddressbook($aArgs['User']->Id);
                    if ($addressbook && $addressbook['id'] == $aArgs['AddressBookId']) {
                        $mResult = true;

                        return $mResult;
                    }
                }
            }
        };
    }

    /**
     *
     */
    public function onBeforeUpdateAddressbookShare(&$aArgs)
    {
        if (isset($aArgs['Id'], $aArgs['UserId'])) {
            $aStorageParts = \explode('-', $aArgs['Id']);
            if (count($aStorageParts) > 1) {
                $iAddressBookId = $aStorageParts[1];

                if (!is_numeric($iAddressBookId)) {
                    return;
                }
                $aArgs['Id'] = $iAddressBookId;
            } elseif (isset($aStorageParts[0])) {
                $storagesMapToAddressbooks = ContactsModule::Decorator()->GetStoragesMapToAddressbooks();
                if (isset($storagesMapToAddressbooks[$aStorageParts[0]])) {
                    $addressbookUri = $storagesMapToAddressbooks[$aStorageParts[0]];
                    $userPublicId = Api::getUserPublicIdById($aArgs['UserId']);
                    if ($userPublicId) {
                        $row = Capsule::connection()->table('adav_addressbooks')
                            ->where('principaluri', Constants::PRINCIPALS_PREFIX . $userPublicId)
                            ->where('uri', $addressbookUri)
                            ->select('adav_addressbooks.id as addressbook_id')->first();
                        if ($row) {
                            $aArgs['Id'] = $row->addressbook_id;
                        }
                    }
                }
            }
        }
    }

    public function onAfterGetStoragesMapToAddressbooks(&$aArgs, &$mResult)
    {
        $mResult = array_merge($mResult, $this->storagesMapToAddressbooks);
    }
}