<?php

namespace KupShop\GraphQLBundle\ApiAdmin\Util;

use KupShop\CatalogBundle\Query\Search;
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserBonusPointCollection;
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserCollection;
use KupShop\GraphQLBundle\ApiAdmin\Types\Parameters;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Enum\BonusPointStatus;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\AddressInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserAddressInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserBonusPointFilterInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserGroupsRelationInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserNewsletterInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserAddressMutateResponse;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserMutateResponse;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\User;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\UserBonusPoint;
use KupShop\GraphQLBundle\ApiShared\ApiUtil;
use KupShop\GraphQLBundle\ApiShared\Util\ParametersAssembler;
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
use KupShop\GraphQLBundle\Exception\GraphQLValidationException;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Email\PasswordResetAdminEmail;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Mail\EmailCheck;
use KupShop\KupShopBundle\Util\ObjectUtil;
use KupShop\PricelistBundle\Util\PriceListWorker;
use KupShop\UserBundle\Security\LegacyPasswordEncoder;
use KupShop\UserBundle\Util\UserConsent;
use Query\Operator;
use Query\QueryBuilder;
use Symfony\Contracts\Service\Attribute\Required;

class UserUtil
{
    /**
     * @var ParametersAssembler
     */
    private $parametersAssembler;

    #[Required]
    public PasswordResetAdminEmail $passwordResetEmail;

    public function __construct(
        private readonly EmailCheck $emailCheck,
        private readonly UserConsent $userConsent,
        private readonly LegacyPasswordEncoder $passwordEncoder,
        private readonly ?PriceListWorker $priceListWorker = null,
        private readonly ?UserAddressesUtil $userAddressesUtil = null,
    ) {
    }

    public function getUserCollection(int $offset, int $limit, ?Parameters $userSort, ?Parameters $userFilter): UserCollection
    {
        $filterSpec = $this->createUserCollectionFilter($offset, $limit, $userSort, $userFilter);

        $users = $this->getUsers($filterSpec, $totalCount);

        return (new UserCollection($users))
            ->setItemsTotalCount($totalCount)
            ->setLimit($limit)
            ->setOffset($offset);
    }

    public function getUser(?int $id, ?string $email, ?string $cartId = null): ?User
    {
        if (!$id && !$email && !$cartId) {
            throw new GraphQLValidationException('Cannot query `user` without id or email or cartId provided!');
        }

        $users = $this->getUsers(Operator::equals(array_filter(['u.id' => $id, 'u.email' => $email,  'u.user_key' => $cartId]), 'OR'));

        if (!($user = reset($users))) {
            throw new GraphQLNotFoundException('User not found!');
        }

        return $user;
    }

    /**
     * @return User[]
     */
    public function getUsers(callable $filterSpec, ?int &$totalCount = null): array
    {
        $useTotalCount = count(func_get_args()) > 1;

        $qb = sqlQueryBuilder()
            ->addCalcRows()
            ->select('u.*')
            ->from('users', 'u')
            ->andWhere($filterSpec)
            ->groupBy('u.id');

        $users = [];
        foreach ($qb->execute() as $item) {
            $user = new \User();
            $user->loadData($item);
            $user->fetchAddresses($item);

            $users[$item['id']] = $user;
        }

        if ($useTotalCount) {
            $totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
        }

        $this->processUsersMultiFetches($users);

        return ApiUtil::wrapItems($users, User::class);
    }

    public function getUsersBonusPoints(int $offset, int $limit, ?Parameters $sort, ?UserBonusPointFilterInput $filter): UserBonusPointCollection
    {
        $qb = sqlQueryBuilder()
            ->addCalcRows()
            ->select('bp.*')
            ->from('bonus_points', 'bp')
            ->andWhere(ApiUtil::getLimitSpec($offset, $limit))
            ->andWhere(ApiUtil::getSortSpec($sort, 'bp'));

        if ($filter?->id) {
            $qb->andWhere(Operator::inIntArray($filter->id, 'bp.id'));
        }

        if ($filter?->userId) {
            $qb->andWhere(Operator::inIntArray($filter->userId, 'bp.id_user'));
        }

        if ($filter?->orderId) {
            $qb->andWhere(Operator::inIntArray($filter->orderId, 'bp.id_order'));
        }

        if ($filter?->status) {
            $qb->andWhere(Operator::inStringArray(array_map(fn (BonusPointStatus $status) => $status->value, $filter->status), 'bp.status'));
        }

        if ($filter?->dateCreated) {
            $qb->andWhere(ApiUtil::getDateTimeFilter($filter->dateCreated, 'bp.date_created'));
        }

        $data = $qb->execute();
        $totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];

        return (new UserBonusPointCollection(
            ApiUtil::wrapItems($data, UserBonusPoint::class))
        )
            ->setOffset($offset)
            ->setLimit($limit)
            ->setItemsTotalCount($totalCount);
    }

    public function addOrRemoveUserGroupsRelations(UserGroupsRelationInput $input): UserMutateResponse
    {
        // zkontroluju, ze uzivatel existuje
        $this->validateUser($input->userId);

        // zkontroluju, ze vsechny uzivatelske skupiny existuji
        foreach ($input->userGroupIds as $group) {
            $this->validateUserGroup($group);
        }

        if ($input->overwrite) {
            sqlQueryBuilder()
                ->delete('users_groups_relations')
                ->where(Operator::equals(['id_user' => $input->userId]))
                ->execute();
        }

        foreach ($input->userGroupIds as $group) {
            sqlQueryBuilder()
                ->insert('users_groups_relations')
                ->directValues([
                    'id_user' => $input->userId,
                    'id_group' => $group,
                ])
                ->onDuplicateKeyUpdate(['id_group' => $group, 'id_user' => $input->userId])
                ->execute();
        }

        return new UserMutateResponse(true, $this->getUpdatedUser($input->userId));
    }

    public function createOrUpdateUser(UserInput $input): UserMutateResponse
    {
        if (empty($input->id) && !ObjectUtil::isPropertyInitialized($input, 'email')) {
            throw new GraphQLValidationException('Field "email" is required when creating user!');
        }

        // zkontroloval, ze uzivatel se zadanym ID existuje
        if (isset($input->id)) {
            $userExists = sqlQueryBuilder()
                ->select('id')
                ->from('users')
                ->where(Operator::equals(['id' => $input->id]))
                ->execute()->fetchOne();

            if (!$userExists) {
                throw new GraphQLNotFoundException(sprintf('User with ID "%s" does not exists!', $input->id));
            }
        }

        // zkontroluju mail
        if (ObjectUtil::isPropertyInitialized($input, 'email')) {
            $this->validateUserEmail($input->email, !empty($input->id) ? $input->id : null);
        }

        // zkontroluju, ze zadavam existujici cenik
        if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
            $this->validateUserPriceList($input->priceListId);
        }

        if (ObjectUtil::isPropertyInitialized($input, 'password')) {
            $this->validateUserPassword($input->password);
        }

        $userData = $this->getUserData($input);

        $isNewUser = false;
        $userId = !empty($input->id) ? $input->id : null;
        // vytvoreni zakaznika
        if (!$userId) {
            $defaults = [
                'figure' => 'Y',
                'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'),
            ];

            $isNewUser = true;
            $insert = array_merge($userData, $defaults);

            $userId = sqlGetConnection()->transactional(function () use ($insert) {
                sqlQueryBuilder()
                    ->insert('users')
                    ->directValues($insert)
                    ->execute();

                return (int) sqlInsertId();
            });
        } elseif (!empty($userData)) {
            // aktualizace zakaznika
            sqlQueryBuilder()
                ->update('users')
                ->directValues($userData)
                ->where(Operator::equals(['id' => $userId]))
                ->execute();
        }

        if (ObjectUtil::isPropertyInitialized($input, 'data')) {
            ApiUtil::updateObjectCustomData('users', $userId, $input->data, 'custom_data');
        }

        if (ObjectUtil::isPropertyInitialized($input, 'newsletter')) {
            $this->updateUserNewsletter($userId, $isNewUser, $input->newsletter);
        }

        if ($input->sendPasswordEmail) {
            $this->sendPasswordResetEmail($userId);
        }

        return new UserMutateResponse(true, $this->getUpdatedUser($userId));
    }

    protected function getUserData(UserInput $input): array
    {
        $data = [];

        if (ObjectUtil::isPropertyInitialized($input, 'email')) {
            $data['email'] = $input->email;
        }

        if (ObjectUtil::isPropertyInitialized($input, 'password')) {
            $data['passw'] = $input->password ? $this->passwordEncoder->hash($input->password) : '';
        }

        if (ObjectUtil::isPropertyInitialized($input, 'ico')) {
            $data['ico'] = $input->ico;
        }

        if (ObjectUtil::isPropertyInitialized($input, 'dic')) {
            $data['dic'] = $input->dic;
        }

        if (ObjectUtil::isPropertyInitialized($input, 'isActive')) {
            $data['figure'] = $input->isActive ? 'Y' : 'N';
        }

        if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
            $data['id_pricelist'] = $input->priceListId;
        }

        if (ObjectUtil::isPropertyInitialized($input, 'invoiceAddress')) {
            $address = $this->getUserAddress($input->invoiceAddress);
            if (!empty($address)) {
                $data = array_merge($data, $address);
            }
        }

        if (ObjectUtil::isPropertyInitialized($input, 'deliveryAddress')) {
            $address = $this->getUserAddress($input->deliveryAddress);
            foreach ($address as $key => $value) {
                if (in_array($key, ['ico', 'dic'])) {
                    continue;
                }

                $data['delivery_'.$key] = $value;
            }
        }

        return $data;
    }

    protected function updateUserNewsletter(int $userId, bool $isNewUser, ?UserNewsletterInput $newsletterInput): void
    {
        if (!$newsletterInput) {
            return;
        }

        if (ObjectUtil::isPropertyInitialized($newsletterInput, 'isSubscribed') && $newsletterInput->isSubscribed !== null) {
            $this->userConsent->updateNewsletter(
                userId: $userId,
                newsletter: $newsletterInput->isSubscribed ? 'Y' : 'N',
                new_user: $isNewUser,
                confirmed: true
            );
        }
    }

    public function getUpdatedUser(int $userId): User
    {
        $users = QueryHint::withRouteToMaster(fn () => $this->getUsers(Operator::equals(['u.id' => $userId])));

        return reset($users);
    }

    protected function getUserAddress(AddressInput|UserAddressInput $input): array
    {
        // validovat zemi
        if (ObjectUtil::isPropertyInitialized($input, 'country')) {
            $countries = Contexts::get(CountryContext::class)->getAll();
            if (empty($countries[$input->country])) {
                throw new GraphQLValidationException(sprintf('Country "%s" does not exist!', $input->country));
            }
        }

        return (array) $input;
    }

    protected function prepareUserDeliveryAddress(array $address): array
    {
        $userDeliveryAddress = [];
        if (!$this->checkUser($address['userId']) ?? false) {
            throw new GraphQLValidationException(sprintf('User with ID "%s" does not exist!', $address['userId']));
        }

        $address = $this->renameAddressFields($address);

        foreach ($address as $key => $value) {
            if (in_array($key, ['id', 'id_user'])) {
                $userDeliveryAddress[$key] = $value;
                continue;
            }

            $userDeliveryAddress['delivery_'.$key] = $value;
        }

        return $userDeliveryAddress;
    }

    private function renameAddressFields(array $address)
    {
        $address['id_user'] = $address['userId'];
        $address['custom_address'] = $address['customAddress'] ?? '';

        unset($address['userId']);
        unset($address['customAddress']);

        return $address;
    }

    private function checkUser(int $userId): ?int
    {
        return sqlQueryBuilder()
            ->select('id')
            ->from('users')
            ->where(Operator::equals(['id' => $userId]))
            ->execute()->fetchOne() ?: null;
    }

    private function checkCurrency(string $currency): ?string
    {
        return sqlQueryBuilder()
            ->select('id')
            ->from('currencies')
            ->where(Operator::equals(['id' => $currency]))
            ->execute()->fetchOne() ?: null;
    }

    protected function validateUserPassword(?string $password): void
    {
        if ($password === null) {
            return;
        }

        if (strlen($password) < 6) {
            throw new GraphQLValidationException(
                message: 'Password must be at least 6 characters long.',
                extensions: ['field' => 'password'],
            );
        }
    }

    protected function validateUserEmail(?string $email, ?int $userId = null): void
    {
        if (empty($email)) {
            throw new GraphQLValidationException('Field "email" cannot be empty!');
        }

        $spec = [
            Operator::equals(['email' => $email]),
        ];

        if ($userId) {
            $spec[] = Operator::not(
                Operator::equals(['id' => $userId])
            );
        }

        // kontrola, zda se nesnazim nastavit email, ktery uz ma jiny zakaznik
        $emailExists = sqlQueryBuilder()
            ->select('id')
            ->from('users')
            ->where(Operator::andX($spec))
            ->execute()->fetchOne();

        if ($emailExists) {
            throw new GraphQLValidationException(
                sprintf('Email "%s" is already set up for another user!', $email)
            );
        }

        if (!$this->emailCheck->isEmailDomainValid($email)) {
            throw new GraphQLValidationException(sprintf('Email "%s" is not valid!', $email));
        }
    }

    protected function validateUser(int $userId): void
    {
        $userExists = sqlQueryBuilder()
            ->select('id')
            ->from('users')
            ->where(Operator::equals(['id' => $userId]))
            ->sendToMaster()->execute()->fetchOne();

        if (!$userExists) {
            throw new GraphQLNotFoundException(sprintf(
                'User with ID "%s" does not exist!', $userId)
            );
        }
    }

    protected function validateUserGroup(int $userGroup): void
    {
        $userGroupExists = sqlQueryBuilder()
            ->select('id')
            ->from('users_groups')
            ->where(Operator::equals(['id' => $userGroup]))
            ->execute()->fetchOne();

        if (!$userGroupExists) {
            throw new GraphQLValidationException(
                sprintf('User group with ID "%s" does not exist!', $userGroup)
            );
        }
    }

    protected function validateUserPriceList(?int $priceListId): void
    {
        if (!$this->priceListWorker) {
            return;
        }

        if ($priceListId !== null && !$this->priceListWorker->getPriceList($priceListId)) {
            throw new GraphQLValidationException(
                sprintf('Price list with ID "%s" does not exists!', $priceListId)
            );
        }
    }

    protected function createUserCollectionFilter(int $offset, int $limit, ?Parameters $sort, ?Parameters $filter): callable
    {
        $phone = $filter?->get('phone');
        if (!empty($phone)) {
            $filter->set('phone', Operator::like(['u.phone' => '%'.$phone.'%']));
        } else {
            $filter?->remove('phone');
        }

        if (!empty($filter?->get('name'))) {
            $filter->set('name', Search::searchFields($filter->get('name'), [
                ['field' => 'u.name', 'match' => 'both'],
                ['field' => 'u.surname', 'match' => 'both'],
            ]));
        } else {
            $filter?->remove('name');
        }

        return function (QueryBuilder $qb) use ($offset, $limit, $sort, $filter) {
            $this->parametersAssembler->assemblyInput($filter, $sort, $offset, $limit, $qb);
        };
    }

    protected function saveDeliveryAddress(array $address): int
    {
        return sqlGetConnection()->transactional(function () use ($address) {
            $qb = sqlQueryBuilder();
            if ($id = ($address['id'] ?? false)) {
                unset($address['id']);
                $qb->update('users_addresses')
                    ->directValues($address)
                    ->where(Operator::equals(['id' => $id]))
                    ->execute();

                return $id;
            }

            $qb->insert('users_addresses')
                ->directValues($address)
                ->execute();

            return sqlInsertId();
        });
    }

    public function createOrUpdateUserAddress(UserAddressInput $input): UserAddressMutateResponse
    {
        $address = $this->getUserAddress($input);

        if (isset($address['id'])) {
            $addressExists = sqlQueryBuilder()
                ->select('id')
                ->from('users_addresses')
                ->where(Operator::equals(['id' => $address['id']]))
                ->execute()->fetchOne();

            if (!$addressExists) {
                throw new GraphQLNotFoundException(sprintf('Address with ID "%s" does not exists!', $address['id']));
            }
        }

        $address = $this->prepareUserDeliveryAddress($address);

        return new UserAddressMutateResponse(true, $this->saveDeliveryAddress($address));
    }

    protected function processUsersMultiFetches(array &$users): void
    {
    }

    /**
     * @required
     */
    public function setParametersAssembler(ParametersAssembler $parametersAssembler): void
    {
        $this->parametersAssembler = $parametersAssembler;
    }

    public function sendPasswordResetEmail(int $userId): void
    {
        $user = QueryHint::withRouteToMaster(fn () => \User::createFromId($userId));
        $this->passwordResetEmail->setUser($user);

        $email = $this->passwordResetEmail->getEmail();

        $email['to'] = $user->email;

        $this->passwordResetEmail->sendEmail($email);
    }
}
