package ru.yandex.qe.dispenser.domain.dao.quota;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import javax.ws.rs.NotSupportedException;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import ru.yandex.qe.dispenser.domain.EntitySpec;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.Quota;
import ru.yandex.qe.dispenser.domain.QuotaSpec;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.Segment;

public class MixedQuotaDao extends QuotaDaoImpl {
    private static final Logger LOG = LoggerFactory.getLogger(MixedQuotaDao.class);

    private final ConcurrentMap<Key, Long> qmapping = new ConcurrentHashMap<>();

    @Autowired
    private SqlQuotaDao sqlQuotaDao;

    private volatile Table<QuotaSpec, Project, Long> prevState = HashBasedTable.create();

    @Override
    @NotNull
    public Set<Quota> getQuotas(final @NotNull Resource resource, @NotNull final Collection<Project> projects, @NotNull final Set<Segment> segments) {
        final Set<Quota> result = new HashSet<>();
        for (final Project p : projects) {
            final Long id = qmapping.get(Key.of(resource, p, segments));
            if (id != null) {
                result.add(read(id));
            }
        }
        return result;
    }


    @Transactional
    public void dump(@NotNull final Collection<EntitySpec> specs) {
        final Set<Resource> resources = specs.stream().flatMap(s -> s.getResources().stream()).collect(Collectors.toSet());
        final Table<QuotaSpec, Project, Long> actuals = HashBasedTable.create();
        final Map<Quota.Key, Long> diff = new HashMap<>();

        filter(q -> resources.contains(q.getResource()))
                .forEach(q -> {
                    final Long prevActual = prevState.get(q.getSpec(), q.getProject());
                    if (prevActual == null || !prevActual.equals(q.getOwnActual())) {
                        diff.put(q.getKey(), q.getOwnActual());
                    }
                    actuals.put(q.getSpec(), q.getProject(), q.getOwnActual());
                });
        if (!diff.isEmpty()) {
            applyChanges(Collections.emptyMap(), diff, Collections.emptyMap());
        }
        prevState = actuals;
    }


    @Override
    public void createZeroQuotasFor(final @NotNull QuotaSpec quotaSpec) {
        sqlQuotaDao.createZeroQuotasFor(quotaSpec);
        super.createZeroQuotasFor(quotaSpec);
    }

    @Override
    public void createZeroQuotasFor(final @NotNull Collection<Project> projects) {
        sqlQuotaDao.createZeroQuotasFor(projects);
        super.createZeroQuotasFor(projects);
    }

    @NotNull
    @Override
    @Transactional
    public Quota create(final @NotNull Quota quota) {
        final Quota newQuota = sqlQuotaDao.create(quota);
        qmapping.put(Key.of(newQuota), newQuota.getId());
        return super.createUnsafe(newQuota.getId(), newQuota);
    }

    @Override
    public boolean delete(final @NotNull Quota obj) {
        throw new UnsupportedOperationException("quota deletetion is unsupported");
    }

    @NotNull
    @Override
    public Quota createIfAbsent(@NotNull final Quota quota) {
        throw new NotSupportedException();
    }

    public void sync() {
        LOG.debug("sync quotas with db");
        sqlQuotaDao.getAll().stream().filter(q -> !id2obj.containsKey(q.getId())).forEach(q -> {
            Quota zeroQuota = Quota.builder(q).ownActual(0L).build();
            assert zeroQuota.getId() == q.getId();
            zeroQuota = createUnsafe(zeroQuota.getId(), zeroQuota);
            qmapping.put(Key.of(zeroQuota), zeroQuota.getId());
        });
        LOG.debug("done");
    }

    private static class Key {
        final Project p;
        final Resource r;
        final Set<Segment> s;

        private final int hash;

        private Key(final Project p, final Resource r, final Set<Segment> segments) {
            this.p = p;
            this.r = r;
            this.hash = Objects.hash(p, r);
            this.s = segments;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final Key key = (Key) o;
            return Objects.equals(p, key.p) &&
                    Objects.equals(r, key.r) &&
                    Objects.equals(s, key.s);
        }

        @Override
        public int hashCode() {
            return hash;
        }

        public static Key of(final Quota q) {
            return new Key(q.getProject(), q.getSpec().getResource(), q.getSegments());
        }

        public static Key of(final @NotNull Resource r, final Project p, @NotNull final Set<Segment> segments) {
            return new Key(p, r, segments);
        }
    }
}
