package ru.yandex.qe.dispenser.ws.quota;

import java.util.ArrayDeque;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.google.common.collect.Sets;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.domain.Quota;
import ru.yandex.qe.dispenser.domain.QuotaSpec;
import ru.yandex.qe.dispenser.domain.dao.quota.QuotaDao;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.ws.intercept.TransactionWrapper;

/**
 * Updates quota maxes for services (providers) which doesn't use projects hierarchy.
 */
@Component
@Profile("secondary")
public class QuotaMaxAggregationJob {
    private static final Logger LOG = LoggerFactory.getLogger(QuotaMaxAggregationJob.class);

    private final QuotaDao quotaDao;
    private final boolean enabled;
    private final @NotNull HierarchySupplier hierarchySupplier;

    @Inject
    public QuotaMaxAggregationJob(@NotNull final QuotaDao quotaDao,
                                  @NotNull final HierarchySupplier hierarchySupplier,
                                  @Value("${quotas.aggregation.forProviders.withNoHierarchy.enabled}") final boolean enabled) {
        this.quotaDao = quotaDao;
        this.enabled = enabled;
        this.hierarchySupplier = hierarchySupplier;
    }

    public void aggregateMaxes() {
        if (!enabled) {
            return;
        }

        LOG.info("Start quota max aggregation");

        hierarchySupplier.renewThreadHierarchy();

        final Set<QuotaSpec> quotaSpecs = Hierarchy.get().getQuotaSpecReader().getAll();
        for (final QuotaSpec spec : quotaSpecs) {
            if (!spec.getResource().getService().getSettings().usesProjectHierarchy()) {
                TransactionWrapper.INSTANCE.execute(() -> aggregateMaxes(spec));
            }
        }
        LOG.info("Finish quota max aggregation");
    }

    private void aggregateMaxes(final QuotaSpec quotaSpec) {
        final Set<Quota> quotas = quotaDao.getQuotasForUpdate(quotaSpec);
        final Map<Quota.Key, Quota> quotasByKey = quotas.stream()
                .collect(Collectors.toMap(Quota::getKey, Function.identity()));

        final Set<Quota.Key> leafQuotaKeys = quotas.stream()
                .filter(quota -> quota.getProject().isLeaf())
                .map(Quota::getKey)
                .collect(Collectors.toSet());

        final Queue<Quota.Key> quotaKeysToProcess = new ArrayDeque<>();
        leafQuotaKeys.forEach(quotaKeysToProcess::offer);

        final Map<Quota.Key, Long> aggregatedMaxesToSet = new HashMap<>();
        while (!quotaKeysToProcess.isEmpty()) {
            final Quota.Key quotaKey = quotaKeysToProcess.poll();
            final Set<Quota.Key> subProjectQuotaKeys = quotaKey.getProject()
                    .getSubProjects()
                    .stream()
                    .map(quotaKey::withProject)
                    .collect(Collectors.toSet());
            final Set<Quota.Key> quotasWithNotCalculatedMax = Sets.difference(subProjectQuotaKeys, aggregatedMaxesToSet.keySet());
            if (quotasWithNotCalculatedMax.isEmpty()) {
                final long subprojectsMaxSum = subProjectQuotaKeys.stream()
                        .mapToLong(aggregatedMaxesToSet::get)
                        .sum();
                final long ownMax = quotasByKey.get(quotaKey).getOwnMax();
                final long maxValue = subprojectsMaxSum + ownMax;
                aggregatedMaxesToSet.put(quotaKey, maxValue);
                quotaKeysToProcess.remove(quotaKey);
                if (!quotaKey.getProject().isRoot()) {
                    final Quota.Key parentQuotaKey = quotaKey.withProject(quotaKey.getProject().getParent());
                    if (!quotaKeysToProcess.contains(parentQuotaKey)) {
                        quotaKeysToProcess.offer(parentQuotaKey);
                    }
                }
            } else {
                quotasWithNotCalculatedMax.forEach(key -> {
                    if (!quotaKeysToProcess.contains(key)) {
                        quotaKeysToProcess.offer(key);
                    }
                });
            }
        }
        quotaDao.applyChanges(aggregatedMaxesToSet, Collections.emptyMap(), Collections.emptyMap());
    }
}
