package ru.yandex.qe.dispenser.domain.entity;

import java.time.Clock;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.inject.Inject;

import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;

import ru.yandex.qe.dispenser.api.v1.DiQuotaChangeCause;
import ru.yandex.qe.dispenser.api.v1.request.DiProcessingMode;
import ru.yandex.qe.dispenser.domain.Entity;
import ru.yandex.qe.dispenser.domain.EntitySpec;
import ru.yandex.qe.dispenser.domain.dao.entity.EntityDao;
import ru.yandex.qe.dispenser.domain.dao.entity.EntityFilteringParams;
import ru.yandex.qe.dispenser.domain.dao.entity.spec.EntitySpecUtils;
import ru.yandex.qe.dispenser.domain.distributed.Identifier;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.solomon.SolomonHolder;
import ru.yandex.qe.dispenser.domain.support.EntityOperation;
import ru.yandex.qe.dispenser.domain.support.EntityRelease;
import ru.yandex.monlib.metrics.histogram.Histograms;
import ru.yandex.monlib.metrics.labels.Labels;
import ru.yandex.monlib.metrics.primitives.Histogram;
import ru.yandex.monlib.metrics.primitives.Rate;
import ru.yandex.monlib.metrics.registry.MetricRegistry;


public class EntityLifetimeManagerImpl implements EntityLifetimeManager {
    private static final Logger LOG = LoggerFactory.getLogger(EntityLifetimeManagerImpl.class);

    private static final int MAX_READ_BATCH_SIZE = 1000;
    private static final int MAX_RELEASE_BATCH_SIZE = 100;

    public static final String SENSOR_PREFIX = "entity_lifetime_manager_remove_old_task.";
    public static final String ELAPSED_TIME_SENSOR = SENSOR_PREFIX + "elapsed_time";
    public static final String ERROR_RATE_SENSOR = SENSOR_PREFIX + "error_rate";
    public static final String LAST_START_SENSOR = SENSOR_PREFIX + "time_since_last_start";
    public static final String LAST_SUCCESS_SENSOR = SENSOR_PREFIX + "time_since_last_success_end";

    @Autowired
    private EntityDao entityDao;

    @Autowired
    private Identifier identifier;

    private final Histogram elapsedTime;
    private final Rate errorRate;
    private volatile long lastStart;
    private volatile long lastSuccessEnd;
    private final Clock clock = Clock.systemDefaultZone();
    private final Stopwatch stopwatch = Stopwatch.createUnstarted();

    private final Map<Pair<String, String>, Long> entitySpecToTtl = new HashMap<>();

    @Inject
    public EntityLifetimeManagerImpl(final SolomonHolder solomonHolder) {
        entitySpecToTtl.put(Pair.of("nirvana", "nirvana-workflow-create"), TimeUnit.MINUTES.toMillis(10));
        entitySpecToTtl.put(Pair.of("nirvana", "nirvana-workflow-start"), TimeUnit.MINUTES.toMillis(10));
        entitySpecToTtl.put(Pair.of("scraper", "scraper-batch"), TimeUnit.DAYS.toMillis(1));
        entitySpecToTtl.put(Pair.of("scraper", "get-batch-status"), TimeUnit.MINUTES.toMillis(1));
        entitySpecToTtl.put(Pair.of("scraper", "serps-without-resources"), TimeUnit.MINUTES.toMillis(1));

        final MetricRegistry rootRegistry = solomonHolder.getRootRegistry();
        elapsedTime = rootRegistry.histogramRate(ELAPSED_TIME_SENSOR, Labels.of(), Histograms.exponential(22, 2, 1.0d));
        errorRate = rootRegistry.rate(ERROR_RATE_SENSOR, Labels.of());
        lastStart = clock.millis();
        rootRegistry.lazyGaugeInt64(LAST_START_SENSOR, Labels.of(), () -> clock.millis() - lastStart);
        lastSuccessEnd = clock.millis();
        rootRegistry.lazyGaugeInt64(LAST_SUCCESS_SENSOR, Labels.of(), () -> clock.millis() - lastSuccessEnd);
    }

    @Override
    public void removeOld() {
        stopwatch.start();
        lastStart = clock.millis();
        boolean success = false;
        try {
            if (Hierarchy.get().isSql()) {
                throw new IllegalStateException("Can't remove entity for SqlHierarchy");
            }

            final Set<String> allowedSpecs = Hierarchy.get().getEntitySpecReader().getAll()
                    .stream()
                    .filter(spec -> spec.getTag().equals(identifier.tag()))
                    .map(spec -> spec.getKey().getPublicKey())
                    .collect(Collectors.toSet());

            entitySpecToTtl.forEach((keys, ttl) -> {
                final EntitySpec entitySpec;
                try {
                    entitySpec = EntitySpecUtils.toEntitySpec(keys.getLeft(), keys.getRight());
                } catch (EmptyResultDataAccessException ignored) {
                    LOG.debug("ignoring {} {}", keys.getLeft(), keys.getRight());
                    return;
                }
                if (!allowedSpecs.contains(entitySpec.getKey().getPublicKey())) {
                    LOG.debug("ignoring {}", entitySpec.getKey());
                    return;
                }
                final long ts = System.currentTimeMillis() - ttl;
                final EntityFilteringParams params = EntityFilteringParams.builder()
                        .createdTo(ts)
                        .limit(MAX_READ_BATCH_SIZE) // no offset, previous entities are already deleted
                        .build();
                clean(entitySpec, params);
            });
            for (final EntitySpec entitySpec : Hierarchy.get().getEntitySpecReader().getAll()) {
                if (entitySpec.isExpirable()) {
                    final EntityFilteringParams params = EntityFilteringParams.builder()
                            .expiredTo(System.currentTimeMillis())
                            .limit(MAX_READ_BATCH_SIZE)
                            .build();
                    clean(entitySpec, params);
                }
            }
            success = true;
        } finally {
            if (!success) {
                errorRate.inc();
            } else {
                lastSuccessEnd = clock.millis();
            }
            final long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            elapsedTime.record(elapsed);
            stopwatch.reset();
        }
    }

    private void clean(@Nonnull final EntitySpec entitySpec, @Nonnull final EntityFilteringParams params) {
        while (true) {
            final Set<Entity> entities = entityDao.filter(entitySpec, params);
            if (!entities.isEmpty()) {
                LOG.debug("Removing {} entities of specification {}", entities.size(), entitySpec.getKey());
                remove(entities);
            }
            if (entities.size() < MAX_READ_BATCH_SIZE) {
                break;
            }
        }
    }

    private void remove(@NotNull final Collection<Entity> entities) {
        final List<EntityOperation> releaseOperations = entities.stream().map(this::toEntityReleaseOperation).collect(Collectors.toList());
        final List<List<EntityOperation>> releaseBatches = Lists.partition(releaseOperations, MAX_RELEASE_BATCH_SIZE);
        releaseBatches.forEach(releaseBatch -> {
            entityDao.doChanges(releaseBatch);
        });
    }

    @NotNull
    private EntityRelease toEntityReleaseOperation(@NotNull final Entity entity) {
        final DiQuotaChangeCause cause = DiQuotaChangeCause.releaseEntity(entity.toView())
                .description("Regular release by EntityLifetimeManager")
                .build();
        return new EntityRelease(DiProcessingMode.IGNORE_UNKNOWN_ENTITIES_AND_USAGES, entity, cause);
    }

    @TestOnly
    public void setTtl(@NotNull final String serviceKey, @NotNull final String entitySpecKey, final long ttlMillis) {
        entitySpecToTtl.put(Pair.of(serviceKey, entitySpecKey), ttlMillis);
    }

    @TestOnly
    public void removeAllTtls() {
        entitySpecToTtl.clear();
    }
}
