package ru.yandex.direct.api.v5.entity.agencyclients.service;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.Multimaps;
import one.util.streamex.StreamEx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import ru.yandex.direct.blackbox.client.BlackboxClient;
import ru.yandex.direct.core.entity.account.score.model.AccountScore;
import ru.yandex.direct.core.entity.account.score.service.AccountScoreService;
import ru.yandex.direct.core.entity.campaign.model.Wallet;
import ru.yandex.direct.core.entity.campaign.service.WalletService;
import ru.yandex.direct.core.entity.cashback.service.CashbackClientsService;
import ru.yandex.direct.core.entity.client.model.AgencyClientRelation;
import ru.yandex.direct.core.entity.client.model.Client;
import ru.yandex.direct.core.entity.client.model.ClientLimits;
import ru.yandex.direct.core.entity.client.model.ClientSpecialLimits;
import ru.yandex.direct.core.entity.client.service.AgencyClientRelationService;
import ru.yandex.direct.core.entity.client.service.ClientLimitsService;
import ru.yandex.direct.core.entity.client.service.ClientService;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.core.entity.user.utils.BlackboxGetPhoneQuery;
import ru.yandex.direct.core.validation.CommonDefectTranslations;
import ru.yandex.direct.currency.Currency;
import ru.yandex.direct.currency.Percent;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.env.EnvironmentType;
import ru.yandex.direct.integrations.configuration.IntegrationsConfiguration;
import ru.yandex.direct.rbac.model.Representative;
import ru.yandex.direct.tvm.TvmIntegration;
import ru.yandex.inside.passport.PassportUid;
import ru.yandex.misc.concurrent.Futures;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static ru.yandex.direct.utils.CommonUtils.nvl;
import static ru.yandex.direct.utils.FunctionalUtils.listToMap;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;


@Component
@ParametersAreNonnullByDefault
public class BaseClientDataFetcher {
    private static final Logger logger = LoggerFactory.getLogger(BaseClientDataFetcher.class);

    private static final int PARTITION_SIZE = 100;

    private final ExecutorService executor;
    private final AgencyClientRelationService agencyClientRelationService;
    private final AccountScoreService accountScoreService;
    private final BlackboxClient blackboxClient;
    private final ClientLimitsService clientLimitsService;
    private final ClientService clientService;
    private final WalletService walletService;
    private final TvmIntegration tvmIntegration;
    private final EnvironmentType environmentType;

    @Autowired
    public BaseClientDataFetcher(AgencyClientRelationService agencyClientRelationService,
                                 AccountScoreService accountScoreService,
                                 BlackboxClient blackboxClient, ClientLimitsService clientLimitsService,
                                 ClientService clientService,
                                 WalletService walletService,
                                 @Qualifier(IntegrationsConfiguration.BLACKBOX_EXECUTOR_SERVICE) ExecutorService executor,
                                 TvmIntegration tvmIntegration,
                                 EnvironmentType environmentType) {
        this.agencyClientRelationService = agencyClientRelationService;
        this.accountScoreService = accountScoreService;
        this.blackboxClient = blackboxClient;
        this.clientLimitsService = clientLimitsService;
        this.clientService = clientService;
        this.walletService = walletService;
        this.executor = executor;
        this.tvmIntegration = tvmIntegration;
        this.environmentType = environmentType;
    }

    @Nonnull
    public Map<Long, String> getSmsPhones(List<Long> requestedSubclientUids) {
        List<PassportUid> passportUids = mapList(requestedSubclientUids, PassportUid::new);

        return StreamEx.ofSubLists(passportUids, PARTITION_SIZE)
                .map(partition -> new BlackboxGetPhoneQuery(blackboxClient, tvmIntegration, environmentType, partition))
                .map(executor::submit)
                .toList() // Collecting futures to invoke all
                .stream()
                .map(Futures::get) // Warning, runtime exception possible
                .flatMap(m -> m.entrySet().stream())
                .filter(e -> e.getValue().isPresent())
                .collect(Collectors.toMap(
                        e -> e.getKey().getUid(),
                        e -> e.getValue().get().getMaskedE164Number().get()));
    }

    @Nonnull
    public Map<ClientId, AccountScore> getAccountScore(List<ClientId> clientIds) {
        return listToMap(accountScoreService.massGetLatestAccountScore(clientIds),
                as -> ClientId.fromLong(as.getClientId()));
    }

    @Nonnull
    public Map<ClientId, ClientSpecialLimits> getClientSpecialLimits(List<ClientId> clientIds) {
        return listToMap(clientLimitsService.massGetClientSpecialLimits(clientIds),
                csl -> ClientId.fromLong(csl.getClientId()));
    }

    @Nonnull
    public Map<ClientId, ClientLimits> getClientLimits(List<ClientId> clientIds) {
        return listToMap(clientLimitsService.massGetClientLimits(clientIds), ClientLimits::getClientId);
    }

    @Nonnull
    public Map<ClientId, Currency> getClientsCurrencies(List<ClientId> clientIds) {
        return clientService.massGetWorkCurrency(clientIds);
    }

    @Nonnull
    public Map<ClientId, Collection<AgencyClientRelation>> getAgencyClientRelation(List<ClientId> clientIds) {
        return Multimaps.index(
                agencyClientRelationService.massGetByClients(clientIds),
                AgencyClientRelation::getClientClientId).asMap();
    }

    @Nonnull
    public Map<ClientId, BigDecimal> getClientsOverdraftRest(
            Supplier<Map<ClientId, Currency>> clientsCurrenciesSupplier,
            Supplier<Map<ClientId, Percent>> clientsNdsSupplier, List<ClientId> clientIds) {
        return clientService.getOverdraftRestByClientIds(clientIds, clientsCurrenciesSupplier.get(),
                clientsNdsSupplier.get());
    }

    @Nonnull
    public Map<ClientId, List<RepresentativeInfo>> constructRepresentativesResponse(
            Collection<Representative> representatives, Map<Long, User> userByUid) {
        return representatives.stream()
                .map(r -> {
                    User user = userByUid.get(r.getUserId());
                    if (user == null) {
                        logger.warn("Can not find any users with uid={}, during obtaining "
                                + "representatives for client {}. Skipping.", r.getUserId(), r.getClientId());
                        return null;
                    }
                    return new RepresentativeInfo(r, user);
                })
                .filter(Objects::nonNull)
                .collect(groupingBy(ri -> ri.getRepresentative().getClientId()));
    }


    /**
     * @param agencyClientId ClientId агентства, должно быть указано, если нужно получить данные видимые
     *                       со стороны агентства
     */
    @Nonnull
    public Map<ClientId, Boolean> getSharedAccountInfo(Supplier<Map<ClientId, Currency>> clientsCurrenciesSupplier,
                                                       Supplier<Map<ClientId, Collection<AgencyClientRelation>>> agencyClientRelationsSupplier,
                                                       @Nullable ClientId agencyClientId, List<ClientId> clientIds) {
        Map<ClientId, Currency> clientCurrencies = clientsCurrenciesSupplier.get();
        Map<ClientId, Collection<AgencyClientRelation>> agencyClientRelationsByClientId =
                agencyClientRelationsSupplier.get();

        Map<ClientId, Collection<Wallet>> walletsByClientId = Multimaps.index(
                walletService.massGetWallets(clientIds), Wallet::getClientId).asMap();

        return StreamEx.of(clientIds)
                .toMap(clientId -> {
                    Collection<Wallet> wallets = walletsByClientId.getOrDefault(clientId, Collections.emptyList());
                    Collection<AgencyClientRelation> relations = agencyClientRelationsByClientId
                            .getOrDefault(clientId, Collections.emptyList());

                    Currency workCurrency = clientCurrencies.get(clientId);
                    return checkWalletsEnabled(wallets, relations, workCurrency, agencyClientId);
                });
    }

    @Nonnull
    public Map<ClientId, BigDecimal> getAwaitingBonuses(List<Client> clients) {
        return listToMap(clients,
                client -> ClientId.fromLong(client.getClientId()),
                client -> nvl(client.getCashBackAwaitingBonus(), BigDecimal.ZERO));
    }

    @Nonnull
    public Map<ClientId, BigDecimal> getAwaitingBonusesWithoutNds(List<Client> clients,
                                                                   Supplier<Map<ClientId, Percent>> clientsNdsSupplier,
                                                                   Supplier<Map<ClientId, Currency>> clientsCurrenciesSupplier) {
        return listToMap(clients,
                client -> ClientId.fromLong(client.getClientId()),
                client -> {
                    var clientId = ClientId.fromLong(client.getClientId());
                    var currencies = clientsCurrenciesSupplier.get();
                    var ndsMap = clientsNdsSupplier.get();
                    var currency = currencies.get(clientId);
                    var nds = ndsMap.get(clientId);

                    var awaitingBonus = nvl(client.getCashBackAwaitingBonus(), BigDecimal.ZERO);
                    return currency == null || nds == null ?
                            awaitingBonus : CashbackClientsService.subtractNds(awaitingBonus, currency, nds);
                });
    }

    private boolean checkWalletsEnabled(
            Collection<Wallet> clientsWallets,
            Collection<AgencyClientRelation> agenciesRelations,
            @Nullable Currency workCurrency, @Nullable ClientId agencyClientId) {
        Set<Long> boundAgencies = StreamEx.of(agenciesRelations)
                .filter(AgencyClientRelation::getBinded)
                .map(AgencyClientRelation::getAgencyClientId)
                .map(ClientId::asLong)
                .toSet();

        Predicate<Wallet> suitableWallet;
        if (agencyClientId != null) {
            // для агентства считаем только его кошешьки,
            suitableWallet = w -> w.getAgencyClientId() == agencyClientId.asLong();
        } else {
            // для пользователя считаем неагентские кошельки, а также кошельки привязанных агентств
            suitableWallet = w -> !w.isAgencyWallet() || boundAgencies.contains(w.getAgencyClientId());
        }

        // включенные подходящие кошельки в рабочей валюте клиента
        List<Wallet> enabledWalletStream = clientsWallets.stream()
                .filter(Wallet::getEnabled)
                .filter(w -> Objects.equals(workCurrency, w.getCampaignsCurrency()))
                .filter(suitableWallet)
                .collect(toList());

        // кошельки агентств
        long agenciesWalletsNum = enabledWalletStream.stream().filter(Wallet::isAgencyWallet).count();
        if (agenciesWalletsNum > 1) {
            throw new MultipleAgenciesException(
                    CommonDefectTranslations.INSTANCE.notAllowedGetSettingsClientMultipleAgencies());
        }

        return !enabledWalletStream.isEmpty();
    }
}
