package com.atlassian.bitbucket.mesh.config;

import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ThreadLocalRandom;

public class ExponentialBackoff implements Backoff {

    private final long initialDelayMillis;
    private final double jitter;
    private final long maxDelayMillis;
    private final double multiplier;

    @ConstructorBinding
    public ExponentialBackoff(@DefaultValue("30s") @DurationUnit(ChronoUnit.SECONDS) Duration initialDelay,
                              @DefaultValue("15m") @DurationUnit(ChronoUnit.SECONDS) Duration maxDelay,
                              @DefaultValue("1.7") double multiplier,
                              @DefaultValue("0.1") double jitter) {
        this.jitter = Math.max(0.0, jitter);
        this.multiplier = Math.max(0.0, multiplier);

        maxDelayMillis = Math.max(0L, maxDelay.toMillis());
        initialDelayMillis = Math.min(maxDelayMillis, Math.max(0L, initialDelay.toMillis()));
    }

    @Nonnull
    @Override
    public Backoff.Execution start() {
        return new ExponentialExecution(initialDelayMillis, maxDelayMillis, multiplier, jitter);
    }

    private static class ExponentialExecution implements Backoff.Execution {

        private final double jitter;
        private final long maxDelayMillis;
        private final double multiplier;

        private long nextDelayMillis;

        private ExponentialExecution(long initialDelayMillis, long maxDelayMillis, double multiplier, double jitter) {
            this.jitter = jitter;
            this.maxDelayMillis = maxDelayMillis;
            this.multiplier = multiplier;

            nextDelayMillis = initialDelayMillis;
        }

        @Nonnull
        @Override
        public Duration nextDelay() {
            Duration result = Duration.ofMillis(nextDelayMillis);
            nextDelayMillis = Math.min(
                    (long) (jitter(Math.min(maxDelayMillis, nextDelayMillis * multiplier))), maxDelayMillis);
            return result;
        }

        private double jitter(double value) {
            if (value <= 0.0) {
                return 0.0;
            }
            if (jitter <= 0.0) {
                return value;
            }
            double max = value * jitter;
            return value + ThreadLocalRandom.current().nextDouble(max * 2.0) - max;
        }
    }
}
