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

import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import javax.inject.Inject;

import com.google.common.base.Stopwatch;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.solomon.SolomonHolder;
import ru.yandex.qe.dispenser.domain.staff.StaffApi;
import ru.yandex.qe.dispenser.domain.staff.StaffGroup;
import ru.yandex.qe.dispenser.domain.staff.StaffPage;
import ru.yandex.qe.dispenser.domain.staff.StaffPerson;
import ru.yandex.qe.dispenser.ws.TvmDestination;
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;

@Component
public class StaffApiHelper {

    private static final String SORT_FIELDS = String.join(",", "id");
    private static final String GROUP_FIELDS = String.join(",", "id", "url", "type", "is_deleted");
    private static final String PERSON_FIELDS = String.join(",", "id", "login", "uid", "department_group.id", "department_group.url",
            "department_group.type", "department_group.is_deleted", "department_group.ancestors.id", "department_group.ancestors.url",
            "department_group.ancestors.type", "department_group.ancestors.is_deleted", "groups.group.id", "groups.group.url",
            "groups.group.is_deleted", "groups.group.type", "official.is_robot", "official.is_dismissed", "official.affiliation", "is_deleted");
    private static final String ALL = "all";
    private static final String RATE = "http_outgoing.requests_rate";
    private static final String TIME = "http_outgoing.requests_time_millis";
    private static final String ENDPOINT = "endpoint";
    private static final String ENDPOINT_PERSONS = "GET /v3/persons";
    private static final String ENDPOINT_GROUPS = "GET /v3/groups";
    private static final String CODE = "code";
    private static final String CODE_NON_2XX = "non_2xx";
    private static final String DESTINATION = "destination";
    private static final String STAFF_API = "staff-api";

    private final StaffApi staffApi;
    private final TvmDestination staffTvmDestination;
    private final MetricRegistry registry;
    private final Rate totalRate;
    private final Rate totalNon2xxRate;
    private final Histogram totalDuration;
    private final ConcurrentHashMap<String, Rate> perEndpointRates = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Rate> perEndpointNon2xxRates = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Histogram> perEndpointDurations = new ConcurrentHashMap<>();

    @Inject
    public StaffApiHelper(final StaffApi staffApi, @Qualifier("staff-tvm") final TvmDestination staffTvmDestination, final SolomonHolder solomonHolder) {
        this.staffApi = staffApi;
        this.staffTvmDestination = staffTvmDestination;
        this.registry = solomonHolder.getRootRegistry();
        this.totalRate = registry.rate(RATE, Labels.of(CODE, ALL, ENDPOINT, ALL, DESTINATION, STAFF_API));
        this.totalNon2xxRate = registry.rate(RATE, Labels.of(CODE, CODE_NON_2XX, ENDPOINT, ALL, DESTINATION, STAFF_API));
        this.totalDuration = registry.histogramRate(TIME, Labels.of(CODE, ALL, ENDPOINT, ALL, DESTINATION, STAFF_API),
                Histograms.exponential(22, 2.0d, 1.0d));
    }

    public StaffPage<StaffPerson> getPersonsOrderedByIdAsc(final StaffPersonsQuery query, final int limit) {
        final Optional<String> queryString = query.getQuery();
        return staffTvmDestination.runAuthorized(() -> {
            return measure(() -> {
                if (queryString.isPresent()) {
                    return staffApi.getPersons(PERSON_FIELDS, SORT_FIELDS, limit, queryString.get());
                } else {
                    return staffApi.getPersons(PERSON_FIELDS, SORT_FIELDS, limit);
                }
            }, ENDPOINT_PERSONS);
        });
    }

    public StaffPage<StaffPerson> getPersonsOrderedByIdAsc(final StaffPersonsQuery query, final int limit, final long page) {
        final Optional<String> queryString = query.getQuery();
        return staffTvmDestination.runAuthorized(() -> {
            return measure(() -> {
                if (queryString.isPresent()) {
                    return staffApi.getPersons(PERSON_FIELDS, SORT_FIELDS, limit, queryString.get(), page);
                } else {
                    return staffApi.getPersons(PERSON_FIELDS, SORT_FIELDS, limit, page);
                }
            }, ENDPOINT_PERSONS);
        });
    }

    public Optional<StaffPerson> getPersonById(final long id, final boolean withDeleted) {
        final StaffPersonsQuery query = StaffPersonsQuery.builder().deleted(withDeleted).idFilter(StaffPersonsQuery.idEquals(id)).build();
        final StaffPage<StaffPerson> persons = getPersonsOrderedByIdAsc(query, 1);
        return persons.getResult().stream().findFirst();
    }

    public Optional<StaffPerson> getPersonByLogin(final String login, final boolean withDeleted) {
        final StaffPersonsQuery query = StaffPersonsQuery.builder().deleted(withDeleted).login(login).build();
        final StaffPage<StaffPerson> persons = getPersonsOrderedByIdAsc(query, 1);
        return persons.getResult().stream().findFirst();
    }

    public Optional<StaffPerson> getPersonByUid(final String uid, final boolean withDeleted) {
        final StaffPersonsQuery query = StaffPersonsQuery.builder().deleted(withDeleted).uid(uid).build();
        final StaffPage<StaffPerson> persons = getPersonsOrderedByIdAsc(query, 1);
        return persons.getResult().stream().findFirst();
    }

    public StaffPage<StaffGroup> getGroupsOrderedByIdAsc(final StaffGroupsQuery query, final int limit) {
        final Optional<String> queryString = query.getQuery();
        return staffTvmDestination.runAuthorized(() -> {
            return measure(() -> {
                if (queryString.isPresent()) {
                    return staffApi.getGroups(GROUP_FIELDS, SORT_FIELDS, limit, queryString.get());
                } else {
                    return staffApi.getGroups(GROUP_FIELDS, SORT_FIELDS, limit);
                }
            }, ENDPOINT_GROUPS);
        });
    }

    public StaffPage<StaffGroup> getGroupsOrderedByIdAsc(final StaffGroupsQuery query, final int limit, final long page) {
        final Optional<String> queryString = query.getQuery();
        return staffTvmDestination.runAuthorized(() -> {
            return measure(() -> {
                if (queryString.isPresent()) {
                    return staffApi.getGroups(GROUP_FIELDS, SORT_FIELDS, limit, queryString.get(), page);
                } else {
                    return staffApi.getGroups(GROUP_FIELDS, SORT_FIELDS, limit, page);
                }
            }, ENDPOINT_GROUPS);
        });
    }

    public Optional<StaffGroup> getGroupById(final long id, final boolean withDeleted) {
        final StaffGroupsQuery query = StaffGroupsQuery.builder().deleted(withDeleted).idFilter(StaffGroupsQuery.idEquals(id)).build();
        final StaffPage<StaffGroup> groups = getGroupsOrderedByIdAsc(query, 1);
        return groups.getResult().stream().findFirst();
    }

    public Optional<StaffGroup> getGroupByUrl(final String url, final boolean withDeleted) {
        final StaffGroupsQuery query = StaffGroupsQuery.builder().deleted(withDeleted).url(url).build();
        final StaffPage<StaffGroup> groups = getGroupsOrderedByIdAsc(query, 1);
        return groups.getResult().stream().findFirst();
    }

    private <T> T measure(final Supplier<T> supplier, final String endpointKey) {
        final Stopwatch stopwatch = Stopwatch.createStarted();
        totalRate.inc();
        final Rate perEndpointRate = perEndpointRates.computeIfAbsent(endpointKey,
                key -> registry.rate(RATE, Labels.of(CODE, ALL, ENDPOINT, key, DESTINATION, STAFF_API)));
        perEndpointRate.inc();
        boolean success = false;
        try {
            final T result = supplier.get();
            success = true;
            return result;
        } finally {
            if (!success) {
                totalNon2xxRate.inc();
                final Rate perEndpointNon2xxRate = perEndpointNon2xxRates.computeIfAbsent(endpointKey,
                        key -> registry.rate(RATE, Labels.of(CODE, CODE_NON_2XX, ENDPOINT, key, DESTINATION, STAFF_API)));
                perEndpointNon2xxRate.inc();
            }
            stopwatch.stop();
            final long elapsedMillis = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            totalDuration.record(elapsedMillis);
            final Histogram perEndpointDuration = perEndpointDurations.computeIfAbsent(endpointKey,
                    key -> registry.histogramRate(TIME, Labels.of(CODE, ALL, ENDPOINT, key, DESTINATION, STAFF_API),
                            Histograms.exponential(22, 2.0d, 1.0d)));
            perEndpointDuration.record(elapsedMillis);
        }
    }

}
