<?php

declare(strict_types=1);

namespace External\HannahBundle\SAP\Synchronizer;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use External\HannahBundle\SAP\Exception\SAPException;
use External\HannahBundle\SAP\Exception\SAPManyException;
use External\HannahBundle\SAP\MappingType;
use External\HannahBundle\SAP\Util\DataFactory\UserDataFactory;
use External\HannahBundle\SAP\Util\SAPClient;
use KupShop\AdminBundle\Util\ActivityLog;
use Query\Operator;

class UserSynchronizer extends BaseSynchronizer
{
    private const RETRY_COUNT_MAX = 4;

    protected static $type = 'user';

    /** @var SAPClient */
    private $sapClient;

    /** @var UserDataFactory */
    private $userDataFactory;

    /**
     * @required
     */
    public function setUserDataFactory(UserDataFactory $userDataFactory): void
    {
        $this->userDataFactory = $userDataFactory;
    }

    /**
     * @required
     */
    public function setSAPClient(SAPClient $sapClient): void
    {
        $this->sapClient = $sapClient;
    }

    public function processToSAP(): void
    {
        if (isLocalDevelopment()) {
            return;
        }

        $lastSyncTime = $this->getLastSyncTime();

        $orX = ['su.id_sap IS NULL'];
        if ($lastSyncTime) {
            $orX[] = 'u.date_updated IS NOT NULL AND u.date_updated >= :lastSync';
        } else {
            $orX[] = 'u.date_updated IS NOT NULL';
        }

        $qb = sqlQueryBuilder()
            ->select('u.id, su.id_sap')
            ->from('users', 'u')
            ->leftJoin('u', 'sap_users', 'su', 'u.id = su.id_user')
            ->andWhere(Operator::equals(['u.figure' => 'Y']))
            // pokud neni vyplnene jmeno a prijmeni, tak SAP hazi chybu a nechce ho zapsat
            ->andWhere('u.name != "" AND u.surname != ""')
            ->andWhere(Operator::orX($orX));

        if ($lastSyncTime) {
            $qb->addParameters(
                [
                    'lastSync' => date('Y-m-d H:i:s', $lastSyncTime),
                ]
            );
        }

        $newlyAddedUsers = [];

        foreach ($qb->execute() as $item) {
            if ($result = $this->updateUserToSAP($item)) {
                [$user, $isNewlyAddedUser] = $result;

                if ($isNewlyAddedUser) {
                    $newlyAddedUsers[] = $user;
                }
            }
        }

        $this->updateLastSyncTime();

        if (!empty($newlyAddedUsers)) {
            sleep(3);
            // update data of newly added users to SAP (bcs of price level definition)
            foreach ($newlyAddedUsers as $user) {
                $this->updateNewlyAddedUserFromSAP($user);
            }
        }
    }

    /**
     * Zalozi nebo aktualizuje uzivatele v SAPu.
     */
    public function updateUserToSAP(array $item, bool $force = false): ?array
    {
        $user = \User::createFromId($item['id']);

        if ($force === false && $this->getUserTryCount($user) > self::RETRY_COUNT_MAX) {
            return null;
        }

        $isNewlyAddedUser = false;

        try {
            $passwordInSap = $user->getCustomData()['passwordInSap'] ?? null;
            if ($item['id_sap']) {
                $this->logUserUpdate($user, 'updateUserToSAP::customerUpdate');
                // obecny update uzivatele
                $this->sapClient->customerUpdate(
                    $item['id_sap'],
                    $this->userDataFactory->getData(['user' => $user, 'type' => UserDataFactory::DATA_UPDATE]),
                    $this->configuration->getProcessByUser($user)
                );

                // aktualizovat heslo uzivatele, protoze si ho na shopu zmenil
                if (!empty($user->passw) && $user->passw !== $passwordInSap) {
                    $this->logUserUpdate($user, 'updateUserToSAP::customerChangePassword');
                    $this->sapClient->customerChangePassword($item['id_sap'], $user->passw);
                    $user->setCustomData('passwordInSap', $user->passw);
                }

                // resetovat counter pro sapUpdateTry
                $user->setCustomData('sapUpdateTry', null);
            } else {
                // zalozeni uzivatele
                try {
                    $this->logUserUpdate($user, 'updateUserToSAP::customerCreate');
                    $result = $this->sapClient->customerCreate(
                        $this->userDataFactory->getData(['user' => $user]),
                        $this->configuration->getProcessByUser($user)
                    );
                    // resetovat counter pro sapUpdateTry
                    $user->setCustomData('sapUpdateTry', null);
                } catch (SAPManyException $e) {
                    if ($e->hasExceptionWithCode(['477'])) {
                        $sapUserData = $user->getCustomData()['sap'] ?? [];
                        $sapUserData['cards'] = [];
                        $user->setCustomData('sap', $sapUserData);
                    }

                    // zalogovat chybu do Activity logu
                    if (!$e->hasExceptionWithCode(['020', '095'])) {
                        $this->handleActivityLogError(
                            $user,
                            sprintf('[SAP] Chyba během zakládání uživatele "%s" v SAPu: %s', $user->email, $e->getMessage())
                        );

                        return null;
                    }

                    // pokusit se nacist uz existujiciho uzivatele
                    $this->logUserUpdate($user, 'updateUserToSAP::customerValidate');
                    if ($result = $this->sapClient->customerValidate($user->email, $user->passw, $this->configuration->getProcessByUser($user))) {
                        $result = $result->CustomerData;
                    }
                }

                if (!empty($result->CustomerId)) {
                    $this->createUserMapping($user, $result);
                    $isNewlyAddedUser = true;
                }

                $user->setCustomData('passwordInSap', $user->passw);
            }
        } catch (SAPException $e) {
            if (isLocalDevelopment()) {
                throw $e;
            }

            if ($e instanceof SAPManyException) {
                // autofix duplicated sap_users
                if ($item['id_sap'] && $e->hasExceptionWithCode(['020'])) {
                    $sapUsersCount = sqlQueryBuilder()
                        ->select('id_sap')
                        ->from('sap_users')
                        ->where(Operator::equals(['id_user' => $user->id]))
                        ->execute()->rowCount();

                    if ($sapUsersCount > 1) {
                        sqlQueryBuilder()
                            ->delete('sap_users')
                            ->where(Operator::equals(['id_user' => $user->id, 'id_sap' => $item['id_sap']]))
                            ->execute();
                    }
                }
            }

            $this->handleActivityLogError(
                $user,
                sprintf('[SAP] Nepodařilo se synchronizovat uživatele "%s": %s', $user->email, $e->getMessage())
            );
        } catch (\Throwable $e) {
            $this->sentryLogger->captureException($e);
        }

        return [$user, $isNewlyAddedUser];
    }

    public function updateNewlyAddedUserFromSAP(\User $user): void
    {
        try {
            $this->logUserUpdate($user, 'updateNewlyAddedUserFromSAP::customerValidate');
            $result = $this->sapClient->customerValidate(
                mb_strtolower($user->email),
                $user->passw,
                $this->configuration->getProcessByUser($user)
            );
        } catch (SAPManyException $e) {
            $this->sentryLogger->captureException($e);

            return;
        } finally {
            $this->logger->notice('updateNewlyAddedUserFromSAP result', ['UserId' => $user->id, 'UserData' => $result ?? null]);
        }

        $customer = $result->CustomerData;

        $this->sapUtil->updateUser($user, $customer);
    }

    protected function getHandledFields(): array
    {
        return [];
    }

    private function createUserMapping(\User $user, object $sapCustomer): void
    {
        try {
            $this->sapUtil->createMapping(MappingType::USERS, $sapCustomer->CustomerId, (int) $user->id);
        } catch (UniqueConstraintViolationException $e) {
            // pokud SAP ID pro daneho uzivatele uz ma nastavene nekdo jiny
            $duplicatedUser = sqlQueryBuilder()
                ->select('u.id, u.email')
                ->from('users', 'u')
                ->join('u', 'sap_users', 'su', 'su.id_user = u.id')
                ->andWhere(Operator::equals(['su.id_sap' => $sapCustomer->CustomerId]))
                ->execute()->fetchAssociative();

            if (!$duplicatedUser) {
                throw $e;
            }

            if ($duplicatedUser['email'] !== $sapCustomer->PersonalData->Email) {
                sqlGetConnection()->transactional(function () use ($duplicatedUser, $user, $sapCustomer) {
                    $this->sapUtil->deleteMapping(MappingType::USERS, $duplicatedUser['id']);
                    $this->createUserMapping($user, $sapCustomer);
                });
            }
        }
    }

    private function handleActivityLogError(\User $user, string $message): void
    {
        $tryCount = $this->getUserTryCount($user);

        // prestavam zapisivat activity log, dokud se nepodari zapis, aby to nespamovalo silene moc
        if ($tryCount > self::RETRY_COUNT_MAX) {
            return;
        }

        // zapisu do activity logu
        $this->activityLog->addActivityLog(
            ActivityLog::SEVERITY_ERROR,
            ActivityLog::TYPE_SYNC,
            $message
        );

        // updatuju counter na uzivateli
        $user->setCustomData('sapUpdateTry', ++$tryCount);
    }

    private function getUserTryCount(\User $user): int
    {
        return (int) ($user->getCustomData(true)['sapUpdateTry'] ?? 1);
    }

    private function logUserUpdate(\User $user, string $source): void
    {
        if (isLocalDevelopment()) {
            return;
        }

        $this->logger->notice(
            message: 'SAP: User update to SAP; ID: '.$user->id,
            context: [
                'userId' => $user->id,
                'email' => $user->email,
                'source' => $source,
            ]
        );
    }
}
