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

import java.time.Clock;
import java.util.concurrent.TimeUnit;

import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.solomon.SolomonHolder;

@ParametersAreNonnullByDefault
public class ProjectTreeSyncTask {

    private static final Logger LOG = LoggerFactory.getLogger(ProjectTreeSyncTask.class);

    public static final String SENSOR_PREFIX = "abc_services_sync_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";

    public static final String LAST_ADDED_ROLES_SENSOR = SENSOR_PREFIX + "added_roles";
    public static final String LAST_REMOVED_ROLES_SENSOR = SENSOR_PREFIX + "removed_roles";
    public static final String LAST_ROLE_ADD_FAILURES_SENSOR = SENSOR_PREFIX + "role_add_failures";

    @NotNull
    private final ProjectTreeSync projectTreeSync;
    @NotNull
    private final HierarchySupplier hierarchySupplier;
    @NotNull
    private final Clock clock;
    @NotNull
    private final Histogram elapsedTime;
    @NotNull
    private final Rate errorRate;
    @NotNull
    private final Ticker ticker = Ticker.systemTicker();

    private volatile long lastStart;
    private volatile long lastSuccessEnd;
    private volatile long lastAddedRoles;
    private volatile long lastRemovedRoles;
    private volatile long lastRoleAdditionFailures;

    public ProjectTreeSyncTask(final ProjectTreeSync projectTreeSync, final Clock clock,
                               final SolomonHolder solomonHolder, final HierarchySupplier hierarchySupplier) {
        this.projectTreeSync = projectTreeSync;
        this.hierarchySupplier = hierarchySupplier;
        this.clock = clock;
        this.lastSuccessEnd = clock.millis();
        final MetricRegistry rootRegistry = solomonHolder.getRootRegistry();
        this.elapsedTime = rootRegistry.histogramRate(ELAPSED_TIME_SENSOR, Labels.of(), Histograms.exponential(22, 2, 1.0d));
        this.errorRate = rootRegistry.rate(ERROR_RATE_SENSOR, Labels.of());
        this.lastStart = TimeUnit.NANOSECONDS.toMillis(ticker.read());
        rootRegistry.lazyGaugeInt64(LAST_START_SENSOR, Labels.of(), () -> TimeUnit.NANOSECONDS.toMillis(ticker.read()) - lastStart);
        rootRegistry.lazyGaugeInt64(LAST_SUCCESS_SENSOR, Labels.of(), () -> clock.millis() - lastSuccessEnd);

        rootRegistry.lazyGaugeInt64(LAST_ADDED_ROLES_SENSOR, Labels.of(), () -> lastAddedRoles);
        rootRegistry.lazyGaugeInt64(LAST_REMOVED_ROLES_SENSOR, Labels.of(), () -> lastRemovedRoles);
        rootRegistry.lazyGaugeInt64(LAST_ROLE_ADD_FAILURES_SENSOR, Labels.of(), () -> lastRoleAdditionFailures);
    }

    public void update() {
        LOG.info("Syncing services with ABC...");
        final Stopwatch stopwatch = Stopwatch.createStarted();
        lastStart = TimeUnit.NANOSECONDS.toMillis(ticker.read());
        boolean success = false;
        UpdateProjectMembers.RoleChangesHolder roleChangesHolder = new UpdateProjectMembers.RoleChangesHolder();
        try {
            hierarchySupplier.renewThreadHierarchy();
            projectTreeSync.perform(roleChangesHolder);
            success = true;
        } catch (Throwable e) {
            LOG.error("Failed to sync services with ABC", e);
            throw e;
        } finally {
            stopwatch.stop();
            final long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            if (success) {
                lastSuccessEnd = clock.millis();
                LOG.info("Successfully synced services with ABC in {} seconds", TimeUnit.MILLISECONDS.toSeconds(elapsed));
            } else {
                errorRate.inc();
                LOG.info("Failed to sync services with ABC in {} seconds", TimeUnit.MILLISECONDS.toSeconds(elapsed));
            }
            elapsedTime.record(elapsed);
            lastAddedRoles = roleChangesHolder.getAddedRoles();
            lastRemovedRoles = roleChangesHolder.getRemovedRoles();
            lastRoleAdditionFailures = roleChangesHolder.getAdditionFailures();
        }
    }

}
