<?php

declare(strict_types=1);

namespace External\PompoBundle\DRS\Synchronizer;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use External\PompoBundle\Email\UserPOSRegistrationEmail;
use External\PompoBundle\Util\Configuration;
use External\PompoBundle\Util\User\PompoUserUtil;
use External\PompoBundle\Util\User\UserCardUtil;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\StringUtil;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * Synchronizace uzivatelu do DRSu, zpetne z DRSu stahujeme adresy, karty a deti.
 */
class UserSynchronizer extends AbstractSynchronizer
{
    #[Required]
    public PompoUserUtil $pompoUserUtil;
    #[Required]
    public UserCardUtil $userCardUtil;
    #[Required]
    public Configuration $configuration;
    #[Required]
    public UserPOSRegistrationEmail $registrationEmail;
    #[Required]
    public ContextManager $contextManager;

    protected static $type = 'user';

    private ?int $timestampSql = null;
    private ?int $timestampLocal = null;

    private const USER_DELETED_NOTE = 'Uživatel smazán v DRSu';
    private const USER_GROUP_EMPLOYEE = 10247;

    private const MAX_RESULT_ITERATION = 10_000;

    public function process(): void
    {
        $this->processFromDRS();
        $this->timestampLocal = time();
        $this->processToDRS();

        $this->processBonusPointsToDRS();

        $this->updateTimestamps();
    }

    /**
     * Zapisovani novych uzivatelu a aktualizace upravenych uzivatelu na e-shopu do DRSu.
     */
    public function processToDRS(): void
    {
        if (isDevelopment()) {
            return;
        }

        $timestamp = $this->getUserTimestamp()['local'] ?? null;

        $qb = sqlQueryBuilder()
            ->select('u.*, du.id_drs')
            ->from('users', 'u')
            ->leftJoin('u', 'drs_users', 'du', 'du.id_user = u.id')
            ->andWhere('u.date_reg IS NOT NULL')
            ->groupBy('u.id');

        if ($timestamp) {
            // uzivatel byl aktualizovan - provedla se na nem nejaka zmena, takze ho poslu do DRSu
            $qb->andWhere('u.date_updated IS NOT NULL AND u.date_updated >= :lastSync')
                ->setParameter('lastSync', date('Y-m-d H:i:s', $timestamp));

            $qb->setMaxResults(self::MAX_RESULT_ITERATION);
        }

        foreach ($qb->execute() as $item) {
            $customerId = $item['id_drs'];

            if ($customerId && str_contains($item['email'], $customerId) && str_contains($item['email'], 'pompo.cz')) {
                continue;
            }

            $this->debugLogUserChange((int) $item['id'], 'processToDRS::start', [
                'timestamp' => $timestamp,
                'item' => $item,
                'drsId' => $customerId,
            ]);

            // Zamestnanecky ucet - nechceme ho v DRSu aktualizovat, abychom jim neprepisovali nejaky jejich data.
            // 5 cislic a zacina cislem 6
            // NEBO pokud byl uzivatel "smazan" z DRSu, tak ho uz do DRSu nesynchronizujeme
            if ($this->isUserEmployer($customerId ? (int) $customerId : null) || $item['note'] === self::USER_DELETED_NOTE) {
                $this->debugLogUserChange((int) $item['id'], 'processToDRS::skip', [
                    'isUserDeleted' => $item['note'] === self::USER_DELETED_NOTE,
                    'isUserEmployer' => $this->isUserEmployer($customerId ? (int) $customerId : null),
                ]);

                continue;
            }

            $user = new \User();
            $user->loadData($item);

            if (empty($customerId)) {
                // zkusim najit uzivatele v DRSu, abych ho pripadne nezakladal znovu
                if ($customer = $this->drsApi->getUserByEmail($user->email)) {
                    if ($customerId = $customer['@attributes']['contactNumber'] ?? null) {
                        $drsCustomer = $this->drsApi->getSQLUserByContactNumber((int) $customerId);
                        // pokud je ten uzivatel v DRSu smazanej, tak uz ho nejspis i mame na shopu, takze to na nej
                        // nechceme parovat, protoze nedava smysl to parovat na smazanej ucet
                        // takze nastavim $customerId na null, aby se vytvoril novej uzivatel v DRSu
                        if ($drsCustomer && $this->isUserDeletedInDRS($drsCustomer)) {
                            $customerId = null;
                        }
                    }
                }
            }

            $userData = $this->getUserData($user, $customerId ? (int) $customerId : null);

            // Ulozim / updatuju uzivatele v DRSu
            $customerId = $this->saveUser(
                $userData
            );

            $this->debugLogUserChange((int) $item['id'], 'processToDRS::saveUser', [
                'saveUserResult' => $customerId,
                'userData' => $userData,
            ]);

            // Vytvorim si mapping, pokud jeste neexistoval
            if ($customerId && empty($item['id_drs'])) {
                $this->createUserMapping((int) $user->id, $customerId);
            }
        }
    }

    /**
     * Aktualizace bodu smerem z e-shopu do DRSu.
     *
     * Oddeleno od synchronizace uzivatelu do DRSu kvuli problemum, ktere to zpusobovalo. Pri aktualizaci bodu nechceme
     * aktualizovat e-mail, primarne chceme aktualizovat pouze ty body, ale bohuzel je potreba posilat i dalsi udaje.
     */
    public function processBonusPointsToDRS(?int $timestamp = null): void
    {
        if (isDevelopment()) {
            return;
        }

        $timestamp = $timestamp ?: ($this->getUserTimestamp()['local'] ?? null);

        $qb = sqlQueryBuilder()
            ->select('u.*, du.id_drs')
            ->from('users', 'u')
            ->join('u', 'drs_users', 'du', 'du.id_user = u.id')
            ->andWhere('u.date_reg IS NOT NULL')
            ->groupBy('u.id');

        if ($timestamp) {
            // byla provedena nejaka zmena na bodech uzivatele, takze ho chci poslat do DRSu, aby se mu aktualizovali body
            $qb->andWhere('(u.id IN (SELECT bp.id_user FROM bonus_points bp WHERE bp.date_created >= :lastSync GROUP BY bp.id_user))')
                ->setParameter('lastSync', date('Y-m-d H:i:s', $timestamp));

            $qb->setMaxResults(self::MAX_RESULT_ITERATION);
        }

        foreach ($qb->execute() as $item) {
            $customerId = (int) $item['id_drs'];
            if ($this->isUserEmployer($customerId) || $item['note'] === self::USER_DELETED_NOTE) {
                continue;
            }

            $points = $this->getUserBonusPoints((int) $item['id']);

            try {
                $this->drsApi->updateUserBonusPoints($customerId, $points);
            } catch (\Throwable $e) {
                $this->logger->logException($e, sprintf('[DRS] Aktualizace bodů z e-shopu do DRSu se nezdařila: %s', $customerId), [
                    'points' => $points,
                    'drsId' => $customerId,
                ]);
            }

            $this->debugLogUserChange((int) $item['id'], 'processBonusPointsToDRS', [
                'drsId' => $customerId,
                'points' => $points,
            ]);
        }
    }

    /**
     * Stahovani zmen z DRSu k uzivateli na e-shopu.
     */
    public function processFromDRS(?int $timestamp = null, ?int $limit = 1_000): void
    {
        if (!$timestamp) {
            $timestamp = $this->getUserTimestamp()['sql'] ?? null;
        }

        $this->debugLogUserChange(0, 'processFromDRS::start', []);
        $iteration = 0;

        // ziskam si uzivatele, u kterych se neco zmenilo
        // nacitam max `$limit` uzivatelu v ramci jednoho sync runu, abych nebloknul sync v pripade sileneho mnozstvi zmenenych uzivatelu
        $users = $this->drsApi->getUsersWithAdditionalData(
            timestamp: $timestamp,
            limit: $limit,
            onBatchProcessed: function () {
                // aktualizovat sql timestamp v settingach po tom, co se zpracuje batch, aby se to prubezne posouvalo
                $this->updateTimestamps(local: false);
            }
        );

        foreach ($users as $item) {
            $this->debugLogUserChange(0, 'processFromDRS::processing', ['item' => $item]);
            try {
                $this->updateUser($item);
            } catch (\SoapFault|UniqueConstraintViolationException $e) {
                // ignoruju SOAP chyby a duplicity
            }

            // ulozit timestamp, kterej se pak ulozi do DB
            $this->timestampSql = max((int) $item['timestamp'], $this->timestampSql ?? (int) $item['timestamp']);

            if ($this->mode !== self::MODE_FULL) {
                if ($iteration > self::MAX_RESULT_ITERATION) {
                    break;
                }

                $iteration++;
            }
        }

        // zaktivovat uzivatele, kterym se doplnil mail
        $this->updateUsersVisibility();
    }

    public function processDRSUser(int $drsId): void
    {
        if ($user = $this->drsApi->getUser($drsId)) {
            $this->updateUser($user);
        }
    }

    public function updateUser(array $item, array $config = []): void
    {
        try {
            // nactu si uzivatele pres SOAP, protoze tam jsou data, ktere potrebuji a je tezsi se k nim dostat jinak
            if (!($customer = $this->drsApi->getUserById((int) $item['id']))) {
                return;
            }
        } catch (\SoapFault $e) {
            // nejspis se jedna o smazaneho uzivatele, a tak ho nejde pres SOAP nacist, ale potrebuju mu udelat update na shopu i tak
            if (!str_contains($e->getMessage(), 'Odkaz na objekt není nastaven na instanci objektu')) {
                throw $e;
            }

            $customer = [];
        }

        $item['attributes'] = Mapping::mapKeys(
            json_decode(($item['attributes'] ?? null) ?: '', true) ?: [],
            fn ($k, $v) => [$v['name'], $v['value'] ?? null]
        );

        $deleted = $this->isUserDeletedInDRS($item);

        // pokud uzivatel neexistuje, tak ho zkusim zalozit
        if (!($userId = $this->getUserIdByDRSId($item['id']))) {
            // pokud je smazanej, tak ho ani nezakladam
            if ($deleted) {
                return;
            }

            // nemam data podle kterych bych mo mel zalozit
            if (empty($customer)) {
                return;
            }

            // Pokud na eshopu uzivatel neexistuje, tak ho vytvorim
            if (!($userId = $this->createDRSUser($customer))) {
                return;
            }
        }

        // pokud uzivatele v DRSu smazali, tak ho "odstanim" i u nas
        if ($deleted) {
            $this->updateDeletedUser($userId, $item);
            $this->updateUserRelatedData($userId, $item, $customer, $config);

            return;
        }

        $this->debugLogUserChange($userId, 'updateUser', [
            'databaseUser' => $item,
            'apiUser' => $customer,
        ]);

        $update = ['get_news' => ($customer['@attributes']['wantsEmails'] ?? false) === 'True' ? 'Y' : 'N'];
        if (!empty($item['email']) && filter_var($item['email'], FILTER_VALIDATE_EMAIL)) {
            $update['email'] = $item['email'];
        }

        // aktualizace mailu a newsletteru
        $this->updateUserEmail($userId, (int) $item['id'], $update, (string) ($item['Country'] ?? ''));
        // aktualizace skupin uzivatele
        $this->updateUserGroups($userId, $item);

        // aktualizace fakturacnich udaju
        $this->updateUserDRSAddress($userId, [
            'firstName' => $item['FirstName'] ?? '',
            'lastName' => $item['LastName'] ?? '',
            'street' => $item['Street'] ?? '',
            'city' => $item['City'] ?? '',
            'zipCode' => $item['Zip'] ?? '',
            'phone' => html_entity_decode($item['Phone'] ?? ''),
            'country' => $item['Country'] ?? '',
        ]);

        // aktualizace dodaci adresy
        // vypinam synchronizaci dodaci adresy, protoze se nam ji stejnak nedari zapisovat tak, jak bychom potrebovali
        // a akorat pak v uctu na e-shopu mizi zakaznicke adresy, viz. https://klient.wpj.cz/service/task/8390/change/
        // kde jsem to resil...
        //        if (empty($config) || ($config['addresses'] ?? false) === true) {
        //            foreach ($this->getItemsAsIndexedArray($customer['address'] ?? []) as $address) {
        //                $address = $address['@attributes'] ?? [];
        //                // adresa neni aktivni
        //                if ($address['status'] != 1 || $address['addressType'] != 1) {
        //                    continue;
        //                }
        //
        //                $this->updateUserDRSAddress($userId, $address, 'delivery');
        //                // pouze delivery adresa - vic by jich tam ani byt nemelo
        //                break;
        //            }
        //        }

        // aktualizace zakaznicnich karet a deti
        $this->updateUserRelatedData($userId, $item, $customer, $config);
    }

    /**
     * Provedu "smazani" uzivatele v e-shopu.
     */
    public function updateDeletedUser(int $userId, array $customer): void
    {
        sqlQueryBuilder()
            ->update('users')
            ->directValues(
                [
                    'email' => $customer['id'].'@pompo.cz',
                    'figure' => 'N',
                    'note' => self::USER_DELETED_NOTE,
                ]
            )
            ->where(Operator::equals(['id' => $userId]))
            ->execute();
    }

    private function updateUserGroups(int $userId, array $item): void
    {
        sqlGetConnection()->transactional(function () use ($userId, $item) {
            sqlQueryBuilder()
                ->delete('users_groups_relations')
                ->andWhere(Operator::equals(['id_user' => $userId]))
                ->andWhere(Operator::inIntArray([self::USER_GROUP_EMPLOYEE], 'id_group'))
                ->execute();

            if (($item['attributes']['IsEmployee'] ?? false) === 'True') {
                sqlQueryBuilder()
                    ->insert('users_groups_relations')
                    ->directValues(['id_user' => $userId, 'id_group' => self::USER_GROUP_EMPLOYEE])
                    ->execute();
            }
        });
    }

    private function updateUserRelatedData(int $userId, array $item, array $customer, array $config): void
    {
        // aktualizace zakaznicnich karet
        if (empty($config) || ($config['cards'] ?? false) === true) {
            $this->updateUserCards(
                $userId,
                $this->getItemsAsIndexedArray($customer['customercard'] ?? [])
            );
        }

        // aktualizace deti - na prodejne si je uzivatel schopen napr. pridat dite
        if (empty($config) || ($config['children'] ?? false) === true) {
            $this->updateUserChildren(
                $userId,
                $this->prepareChildrenFromDatabaseUser($item)
            );
        }
    }

    private function updateUserCards(int $userId, array $cards): void
    {
        // aktualizace zakaznickych karet
        $currentCards = array_map(fn ($x) => $x['code'], QueryHint::withRouteToMaster(fn () => $this->userCardUtil->getUserCards($userId)));

        // karty, ktere budu na konci mazat
        $cardsToDelete = $currentCards;

        foreach ($cards as $card) {
            $card = $card['@attributes'] ?? [];

            // pokud by nemela cislo karty, tak skipuju
            if (empty($card['number'])) {
                continue;
            }

            // pokud karta uz existuje, tak nic nedelam a jen ji odeberu z karet ke smazani
            if (($cardId = array_search($card['number'], $currentCards)) !== false) {
                unset($cardsToDelete[$cardId]);
                continue;
            }

            if (($card['status'] ?? false) !== '1') {
                continue;
            }

            try {
                $this->createUserCard($userId, $card['number']);
            } catch (UniqueConstraintViolationException) {
            }
        }

        if (!empty($cardsToDelete)) {
            sqlQueryBuilder()
                ->delete('user_cards')
                ->andWhere(Operator::equals(['id_user' => $userId]))
                ->andWhere(Operator::inIntArray(array_keys($cardsToDelete), 'id'))
                ->execute();
        }
    }

    private function updateUserChildren(int $userId, array $children): void
    {
        sqlGetConnection()->transactional(function () use ($children, $userId) {
            $qb = sqlQueryBuilder()
                ->select('*')
                ->from('users_family')
                ->where(Operator::equals(['id_user' => $userId]));

            $childrenByName = [];
            // pred smazanim si deti ulozim, abych mohl prenest custom data
            foreach ($qb->execute() as $item) {
                $childrenByName[trim(mb_strtolower($item['name']))] = $item;
            }

            // smazu vsechny deti, abych je mohl znovu naimportovat - tim se provede update
            sqlQueryBuilder()
                ->delete('users_family')
                ->where(Operator::equals(['id_user' => $userId]))
                ->execute();

            // poukladam deti k nam do tabulky
            foreach ($children as $child) {
                sqlQueryBuilder()
                    ->insert('users_family')
                    ->directValues(
                        [
                            'id_user' => $userId,
                            'name' => $child['ChildName'],
                            'gender' => !empty($child['ChildSex']) ? $child['ChildSex'] == 1 ? 'M' : 'F' : null,
                            'date_birth' => $child['ChildBirthDay'] ?? null,
                            'data' => $childrenByName[trim(mb_strtolower($child['ChildName']))]['data'] ?? null,
                        ]
                    )->execute();
            }
        });
    }

    private function createUserCard(int $userId, string $cardNumber, bool $deep = false): void
    {
        try {
            sqlQueryBuilder()
                ->insert('user_cards')
                ->directValues(
                    [
                        'id_user' => $userId,
                        'code' => $cardNumber,
                    ]
                )->execute();
        } catch (UniqueConstraintViolationException $e) {
            if ($deep) {
                throw $e;
            }

            sqlQueryBuilder()
                ->delete('user_cards')
                ->where(Operator::equals(['code' => $cardNumber]))
                ->execute();

            $this->createUserCard($userId, $cardNumber, true);
        }
    }

    /**
     * Vraci data, ktere se posilaji pro aktualizaci uzivatele v DRSu.
     */
    public function getUserData(\User $user, ?int $customerId): array
    {
        $user->fetchAddresses();

        $userBonusPoints = $this->getUserBonusPoints((int) $user->id);

        $extendedValue = array_filter([
            // datum vytvoreni zakaznika
            $this->getExtendedValueItem('CreatedDate', $user->date_reg ?? ''),
            // odkud byl zakaznik vytvoren - pokud uz existuje, tak CreatedBranch nechci aktualizovat
            $customerId ? null : $this->getExtendedValueItem('CreatedBranch', $this->getCreatedBranch($user)),
            // ico
            $this->getExtendedValueItem('ITIN', $user->invoice['ico'] ?? ''),
            // dic
            $this->getExtendedValueItem('VATID', $user->invoice['dic'] ?? ''),
        ]);

        return [
            'contact' => [
                '@attributes' => [
                    'contactNumber' => $customerId ?: '',
                    'loginId' => $user->email,
                    'email' => $user->email,
                    'branch' => $this->getCreatedBranch($user),
                    'firstName' => $user->invoice['name'] ?? '',
                    'lastName' => $user->invoice['surname'] ?? '',
                    'bonusPoints' => $userBonusPoints,
                    'wantsEmails' => ($user->get_news ?? null) === 'Y' ? 'true' : 'false',
                    'telephone' => $user->invoice['phone'] ?? '',
                    'street' => $this->substr($user->invoice['street'] ?? ''),
                    'city' => $this->substr($user->invoice['city'] ?? ''),
                    'zipCode' => $this->substr($user->invoice['zip'] ?? '', 10),
                    'state' => $user->invoice['country'] ?: 'CZ',
                    'country' => $user->invoice['country'] ?: 'CZ',
                    'sexId' => $this->getUserSexId($user, true),
                    'discount' => '',
                ],
                'mainVariables' => [],
                // adresy uzivatele
                'address' => $this->getUserAddresses($user),
                // karty uzivatele
                'customercard' => $this->getUserCards($user),
                'extendedvalue' => array_merge($extendedValue, $this->getUserChildren($user)),
            ],
        ];
    }

    protected function processItem(array $item): void
    {
        // TODO: Implement processItem() method.
    }

    /**
     * Zalozeni / aktualizace uzivatele v DRSu.
     */
    private function saveUser(array $data): ?int
    {
        try {
            return $this->drsApi->saveUser($data);
        } catch (\SoapFault $e) {
            if (isDevelopment()) {
                throw $e;
            }

            // ignoruju SoapFault, protoze ten jejich SOAP strasne casto failuje
        }

        return null;
    }

    /**
     * Vytvoreni uzivatele v e-shopu.
     */
    private function createDRSUser(array $item): ?int
    {
        // safe-check
        if (!($customerId = ($item['@attributes']['contactNumber'] ?? null))) {
            return null;
        }

        $insert = [
            'email' => $item['@attributes']['email'] ?? null,
            'figure' => 'N',
            'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'),
            'id_language' => null,
        ];

        if (empty($insert['email']) || !filter_var($insert['email'], FILTER_VALIDATE_EMAIL)) {
            $insert['email'] = $customerId.'@pompo.cz';
        }

        if ($recTimeStamp = ($item['@attributes']['recTimeStamp'] ?? null)) {
            if ($recTimeStamp = \DateTime::createFromFormat('Y-m-d\TH:i:s', $recTimeStamp)) {
                $insert['date_reg'] = $recTimeStamp->format('Y-m-d H:i:s');
            }
        }

        // Uzivatele uz muzeme mit v e-shopu, protoze si mohl prihlasit newsletter
        $newsletterUser = $this->getNewsletterUser($insert['email']);

        return sqlGetConnection()->transactional(function () use ($newsletterUser, $customerId, $insert) {
            // Pokud mam uzivatele, kterej se vytvoril pres newsletter
            if ($newsletterUser) {
                // Prenesu nastaveni newsletteru na uzivatele, ktereho budu vytvaret
                $insert['get_news'] = $newsletterUser['get_news'];
                $insert['date_subscribe'] = $newsletterUser['date_subscribe'];
                $insert['date_unsubscribe'] = $newsletterUser['date_unsubscribe'];

                // Smazu toho newsletter uzivatele, abych mohl zalozit uzivatele podle DRSu
                sqlQueryBuilder()
                    ->delete('users')
                    ->andWhere('date_reg IS NULL')
                    ->andWhere(Operator::equals(['id' => $newsletterUser['id']]))
                    ->execute();
            }

            // zalozim uzivatele
            sqlQueryBuilder()
                ->insert('users')
                ->directValues($insert)
                ->execute();

            $userId = (int) sqlInsertId();

            // vytvorim mapping
            sqlQueryBuilder()
                ->insert('drs_users')
                ->directValues(
                    [
                        'id_drs' => $customerId,
                        'id_user' => $userId,
                    ]
                )->execute();

            return $userId;
        });
    }

    /**
     * Zaktivuje neaktivni uzivatele, pokud to jsou registrovani uzivatele a maji vyplneny mail.
     *
     * Pokud se uzivatel totiz zaklada z DRSu, tak ma vetsinou automaticky generovany mail {drs_id}@pompo.cz
     * takze kdyz se pak pozdeji zaktualizuje, tak zustava furt neaktivni. Takovyhle uzivatele prave tady zviditelnime.
     */
    private function updateUsersVisibility(): void
    {
        sqlQueryBuilder()
            ->update('users', 'u')
            ->join('u', 'drs_users', 'du', 'du.id_user = u.id')
            ->directValues(['figure' => 'Y'])
            ->andWhere('u.date_reg IS NOT NULL')
            ->andWhere(Operator::equals(['u.figure' => 'N']))
            ->andWhere('u.email NOT LIKE CONCAT("%", du.id_drs, "%")')
            ->execute();
    }

    /**
     * Pokud se uzivateli v DRSu zmeni e-mail, tak ho prozenu touhle metodou.
     */
    private function updateUserEmail(int $userId, int $drsId, array $update, string $country, bool $deep = false): void
    {
        $this->debugLogUserChange($userId, 'updateUserEmail::start', [
            'drsId' => $drsId,
            'updateData' => $update,
            'deep' => $deep,
        ]);

        // Nacte si aktualni email u uzivatele
        $currentEmail = sqlQueryBuilder()
            ->select('email')
            ->from('users')
            ->where(Operator::equals(['id' => $userId]))
            ->execute()->fetchOne();

        // aktualizace mailu a newsletteru
        $updated = false;
        try {
            sqlQueryBuilder()
                ->update('users')
                ->directValues($update)
                ->where(Operator::equals(['id' => $userId]))
                ->execute();

            $this->debugLogUserChange($userId, 'updateUserEmail::updated', [
                'updateData' => $update,
                'updatedUserEmail' => true,
            ]);

            $updated = true;
        } catch (UniqueConstraintViolationException $e) {
            if (!$deep) {
                // Pokud se uzivateli snazim zmenit email podle DRSu, ale spadne to, protoze v shopu existuje
                // newsletter uzivatel, tak potrebuju newsletter uzivatele prenest k aktualnimu uzivateli a
                // newsletter uzivatele smazat
                if ($newsletterUser = $this->getNewsletterUser($update['email'])) {
                    sqlGetConnection()->transactional(function () use ($userId, $drsId, $country, $update, $newsletterUser) {
                        // Prenesu nastaveni newsletteru na uzivatele, ktereho aktualizuji
                        $update['get_news'] = $newsletterUser['get_news'];
                        $update['date_subscribe'] = $newsletterUser['date_subscribe'];
                        $update['date_unsubscribe'] = $newsletterUser['date_unsubscribe'];

                        // Smazu toho newsletter uzivatele, abych mohl zalozit uzivatele podle DRSu
                        sqlQueryBuilder()
                            ->delete('users')
                            ->andWhere('date_reg IS NULL')
                            ->andWhere(Operator::equals(['id' => $newsletterUser['id']]))
                            ->execute();

                        $this->updateUserEmail($userId, $drsId, $update, $country, true);
                    });

                    return;
                }
            }

            addActivityLog(
                ActivityLog::SEVERITY_ERROR,
                ActivityLog::TYPE_SYNC,
                sprintf('[DRS] Nepodařilo se zaktualizovat e-mail pro uživatele: %s', $userId),
                [
                    'userId' => $userId,
                    'email' => $update['email'] ?? null,
                    'update' => $update,
                ]
            );
        }

        // Pokud se provedla uspesne aktualizace, zmenil se email a uzivatel mel predtim docasny email, tak odesleme mail
        // aby si uzivatel mohl dokoncit registraci
        if ($updated && !empty($update['email']) && preg_match('/^(\d+)@pompo\.cz$/',
            $currentEmail) && $currentEmail !== $update['email'] && filter_var($update['email'], FILTER_VALIDATE_EMAIL)) {
            $this->pompoUserUtil->sendPreregistrationEmail(
                userId: $userId,
                drsId: $drsId,
                email: $update['email'],
                country: !empty($country) ? $country : null,
            );
        }
    }

    /**
     * Uzivatele uz muzeme mit v e-shopu, protoze si mohl prihlasit newsletter.
     */
    private function getNewsletterUser(string $email): ?array
    {
        $shopUser = sqlQueryBuilder()
            ->select('*')
            ->from('users')
            // pokud ma jen newsletter, tak nema datum registrace
            ->andWhere('date_reg IS NULL')
            // selectuju podle mailu
            ->andWhere(Operator::equals(['email' => $email]))
            ->execute()->fetchAssociative();

        if (!$shopUser) {
            return null;
        }

        return $shopUser;
    }

    private function createUserMapping(int $userId, int $drsId, bool $deep = false): void
    {
        try {
            sqlQueryBuilder()
                ->insert('drs_users')
                ->directValues(
                    [
                        'id_drs' => $drsId,
                        'id_user' => $userId,
                    ]
                )->execute();
        } catch (UniqueConstraintViolationException $e) {
            if ($deep) {
                throw $e;
            }

            // mapping uz existuje
            // pokud maping existuje pro neaktivniho uzivatele, tak mapping smazu a vytvorim znovu na novem uzivateli
            $inactiveUser = sqlQueryBuilder()
                ->select('u.id')
                ->from('users', 'u')
                ->join('u', 'drs_users', 'du', 'du.id_user = u.id')
                ->where(
                    Operator::equals(
                        [
                            'du.id_drs' => $drsId,
                            'u.figure' => 'N',
                        ]
                    )
                )->execute()->fetchOne();

            if ($inactiveUser) {
                sqlQueryBuilder()
                    ->delete('drs_users')
                    ->where(Operator::equals(['id_drs' => $drsId]))
                    ->execute();

                $this->createUserMapping($userId, $drsId, true);
            }
        }
    }

    /** Zkontroluje, zda uzivatele v DRSu "smazali" */
    private function isUserDeletedInDRS(array $item): bool
    {
        // do FirstName davaji prefix DEL_<ContactNumber>, kde ContactNumber je zakaznik, se kterym ho treba sloucili
        if (StringUtil::startsWith($item['FirstName'], 'DEL_')) {
            return true;
        }

        return false;
    }

    /** Overi, zda se jedna o zamestnanecky ucet */
    private function isUserEmployer(?int $customerId): bool
    {
        return $customerId && strlen((string) $customerId) == 5 && StringUtil::startsWith((string) $customerId, '6');
    }

    /**
     * Aktualizuje adresu uzivatele na e-shopu podle adresy v DRSu.
     */
    private function updateUserDRSAddress(int $userId, array $address, string $type = 'invoice'): void
    {
        $prefix = '';
        if ($type === 'delivery') {
            $prefix = $type.'_';
        }

        $update = [
            $prefix.'name' => $address['firstName'] ?? '',
            $prefix.'surname' => $address['lastName'] ?? '',
            $prefix.'firm' => $address['company'] ?? '',
            $prefix.'street' => $address['street'] ?? '',
            $prefix.'city' => $address['city'] ?? '',
            $prefix.'zip' => $address['zipCode'] ?? '',
            $prefix.'country' => $address['country'] ?? 'CZ',
        ];

        if (!empty($address['phone'])) {
            $update[$prefix.'phone'] = $address['phone'];
        }

        sqlQueryBuilder()
            ->update('users')
            ->directValues($update)
            ->where(Operator::equals(['id' => $userId]))
            ->execute();
    }

    private function getExtendedValueItem(string $name, string $value): array
    {
        return [
            '@attributes' => [
                'name' => $name,
                'value' => $value,
            ],
        ];
    }

    private function getUserAddresses(\User $user): array
    {
        $result = [];
        // delivery adressu nezapisujeme, protoze to delalo akorat problemy, takze zapisujeme a pracujeme pouze s
        // fakturacni adresou, viz. https://klient.wpj.cz/service/task/8390/change/, kde se odmazavali adresy
        foreach (['invoice'] as $type) {
            $result[] = $this->getUserAddress($user, $type);
        }

        return $result;
    }

    private function getUserAddress(\User $user, string $type = 'invoice'): array
    {
        return [
            '@attributes' => [
                'addressType' => '1',
                'email' => $user->email,
                'firstName' => $user->{$type}['name'] ?? '',
                'lastName' => $user->{$type}['surname'] ?? '',
                'company' => $this->substr($user->{$type}['firm'] ?? '', 33),
                'street' => $this->substr($user->{$type}['street'] ?? ''),
                'city' => $this->substr($user->{$type}['city'] ?? ''),
                'zipCode' => $this->substr($user->{$type}['zip'] ?? '', 10),
                'state' => $user->{$type}['country'] ?: 'CZ',
                'country' => $user->{$type}['country'] ?: 'CZ',
                'houseNumber' => '',
                'orientNumber' => '',
                'status' => '1',
            ],
        ];
    }

    /**
     * Vraci ID shopu, kde byl uzivatel zalozeny.
     */
    private function getCreatedBranch(\User $user): string
    {
        if ($this->configuration->isNejhracka()) {
            if ($user->id_language === 'sk') {
                return '404';
            }

            return '403';
        }

        if ($user->id_language === 'sk') {
            return '402';
        }

        return '401';
    }

    private function getUserCards(\User $user): array
    {
        $result = [];

        $userCards = QueryHint::withRouteToMaster(fn () => $this->userCardUtil->getUserCards($user->id));
        foreach ($userCards as $item) {
            $result[]['@attributes'] = [
                'number' => $item['code'],
                'status' => $item['active'] == 1 ? '1' : '0',
            ];
        }

        return $result;
    }

    private function getUserChildren(\User $user): array
    {
        $indexMap = $this->getChildrenIndexMap();

        $qb = sqlQueryBuilder()
            ->select('*')
            ->from('users_family')
            ->where(Operator::equals(['id_user' => $user->id]))
            ->orderBy('id', 'ASC')
            ->sendToMaster();

        $result = [];
        foreach ($qb->execute() as $index => $item) {
            $prefix = $indexMap[$index] ?? '';

            $result[]['@attributes'] = [
                'name' => $prefix.'ChildBirthDay',
                'value' => $item['date_birth'],
            ];
            $result[]['@attributes'] = [
                'name' => $prefix.'ChildName',
                'value' => $item['name'],
            ];
            $result[]['@attributes'] = [
                'name' => $prefix.'ChildSex',
                'value' => $item['gender'] == 'M' ? '1' : '0',
            ];
        }

        return $result;
    }

    private function getUserBonusPoints(int $userId): int
    {
        return (int) sqlQueryBuilder()
            ->select('SUM(points)')
            ->from('bonus_points')
            ->where(\Query\Operator::equals(['id_user' => $userId, 'status' => 'active']))
            ->execute()->fetchOne() ?? 0;
    }

    private function getUserSexId(\User $user, bool $negativeSex = false): string
    {
        // Co se týče pohlaví, tak v TPOMM je nějaký šotek, kdy v nějakých verzích je to 1 - muž a 2 - žena a někde je to obráceně.
        // bez komentare :)
        if ($user->gender === 'M') {
            return $negativeSex ? '2' : '1';
        }

        if ($user->gender === 'F') {
            return $negativeSex ? '1' : '2';
        }

        return '';
    }

    private function getUserIdByDRSId(string $drsId): ?int
    {
        $id = sqlQueryBuilder()
            ->select('id_user')
            ->from('drs_users')
            ->where(Operator::equals(['id_drs' => $drsId]))
            ->execute()->fetchOne();

        if (!$id) {
            return null;
        }

        return (int) $id;
    }

    private function prepareChildrenFromDatabaseUser(array $user): array
    {
        $data = array_filter($user['attributes'], fn ($x) => str_contains($x, 'Child'), ARRAY_FILTER_USE_KEY);

        $children = [];
        foreach ($this->getChildrenIndexMap() as $index => $value) {
            $children[$index] = [
                'ChildName' => $data["{$value}ChildName"] ?? '',
                'ChildSex' => $data["{$value}ChildSex"] ?? '',
                'ChildBirthDay' => $data["{$value}ChildBirthDay"] ?? '',
            ];
        }

        return array_filter($children, fn ($x) => !empty($x['ChildName']));
    }

    private function prepareChildrenFromExtendedValue(array $extendedValue): array
    {
        $extendedValue = array_filter($extendedValue, function ($x) {
            return strpos($x['@attributes']['name'] ?? '', 'Child') !== false;
        });

        $children = [];
        foreach ($extendedValue as $item) {
            $item = $item['@attributes'] ?? [];
            $childIndex = null;
            $prefix = null;
            foreach ($this->getChildrenIndexMap() as $index => $value) {
                if (StringUtil::startsWith($item['name'] ?? '', $value)) {
                    $childIndex = $index;
                    $prefix = $value;
                    break;
                }
            }

            if ($childIndex === null) {
                continue;
            }

            $childKey = str_replace($prefix, '', $item['name']);

            $children[$childIndex][$childKey] = $item['value'];
        }

        return array_filter($children, function ($x) { return !empty($x['ChildName']); });
    }

    private function getChildrenIndexMap(): array
    {
        return [
            0 => 'First',
            1 => 'Second',
            2 => 'Third',
            3 => 'Fourth',
        ];
    }

    private function getItemsAsIndexedArray(array $items): array
    {
        if (ArrayUtil::isDictionary($items)) {
            $items = [$items];
        }

        return $items;
    }

    private function substr(string $string, int $maxLength = 50): string
    {
        return substr($string, 0, $maxLength);
    }

    private function getUserTimestamp(): ?array
    {
        if ($this->mode === self::MODE_FULL) {
            return null;
        }

        $dbcfg = \Settings::getDefault();

        $drs = $dbcfg->loadValue('drs');

        return $drs[static::getType()]['timestamp'] ?? null;
    }

    // docasne logovani zmen zakazniku
    private function debugLogUserChange(int $userId, string $method, array $data): void
    {
        $this->logger->log(
            '[pompo] User change - ID: '.$userId.'; METHOD: '.$method,
            array_merge($data, ['method' => $method])
        );
    }

    /**
     * Updatuje `user` timestampy v DB settingach.
     */
    private function updateTimestamps(bool $sql = true, bool $local = true): void
    {
        if (!$this->timestampLocal && !$this->timestampSql) {
            return;
        }

        $dbcfg = \Settings::getDefault();

        $drs = $dbcfg->loadValue('drs') ?? [];

        $timestampSQLValue = ($sql && $this->timestampSql) ? $this->timestampSql : ($drs[static::getType()]['timestamp']['sql'] ?? null);
        $timestampLocalValue = ($local && $this->timestampLocal) ? $this->timestampLocal : ($drs[static::getType()]['timestamp']['local'] ?? null);

        $drs[static::getType()]['timestamp'] = [
            'sql' => $timestampSQLValue,
            'local' => $timestampLocalValue,
        ];
        $drs[static::getType()]['updated'] = time();

        $dbcfg->saveValue('drs', $drs, false);
    }
}
