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

import java.net.URI;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;

import com.google.common.base.Stopwatch;
import org.apache.http.HttpStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import ru.yandex.bolts.collection.IteratorF;
import ru.yandex.bolts.collection.ListF;
import ru.yandex.bolts.collection.Option;
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.qe.dispenser.solomon.SolomonHolder;
import ru.yandex.startrek.client.AuthenticatingStartrekClient;
import ru.yandex.startrek.client.Session;
import ru.yandex.startrek.client.StartrekClientBuilder;
import ru.yandex.startrek.client.error.StartrekClientException;
import ru.yandex.startrek.client.model.Comment;
import ru.yandex.startrek.client.model.CommentCreate;
import ru.yandex.startrek.client.model.Issue;
import ru.yandex.startrek.client.model.IssueCreate;
import ru.yandex.startrek.client.model.IssueUpdate;
import ru.yandex.startrek.client.model.Transition;
import ru.yandex.startrek.client.utils.UriBuilder;

@Component
@ParametersAreNonnullByDefault
public class TrackerManagerImpl implements TrackerManager {

    @NotNull
    private final Session session;
    @NotNull
    private final Rate totalRate;
    @NotNull
    private final Rate errorRate;
    @NotNull
    private final Histogram elapsedTime;

    @Inject
    public TrackerManagerImpl(@Value("${tracker.oauth.token}") @NotNull final String token,
                              @Value("${tracker.service.url}") @NotNull final String baseUrl,
                              @Value("${tracker.client.max.connections}") @NotNull final Integer maxConnections,
                              @Value("${tracker.client.connection.timeout.seconds}") @NotNull final Long connectionTimeoutSeconds,
                              @Value("${tracker.client.socket.timeout.milliseconds}") @NotNull final Long socketTimeoutMilliseconds,
                              @NotNull final SolomonHolder solomonHolder) {
        this.session = StartrekClientBuilder.newBuilder()
                .uri(baseUrl)
                .maxConnections(maxConnections)
                .connectionTimeout(connectionTimeoutSeconds, TimeUnit.SECONDS)
                .socketTimeout(socketTimeoutMilliseconds, TimeUnit.MILLISECONDS)
                .userAgent("Dispenser")
                .build(token);
        this.totalRate = solomonHolder.getRootRegistry().rate("tracker_integration.request_rate",
                Labels.of("result", "any"));
        this.errorRate = solomonHolder.getRootRegistry().rate("tracker_integration.request_rate",
                Labels.of("result", "error"));
        this.elapsedTime = solomonHolder.getRootRegistry().histogramRate("tracker_integration.request_duration",
                Labels.of(), Histograms.exponential(22, 2, 1.0d));
    }

    @Override
    public String createIssues(final IssueCreate issueCreate) {
        return withHealthyReport(() -> createIssueOrGetUnique(issueCreate));
    }

    private Option<Issue> findByUnique(final Object unique) {
        final AuthenticatingStartrekClient client = (AuthenticatingStartrekClient) this.session;

        final URI uri = UriBuilder.cons(client.getEndpoint())
                .appendPath("issues")
                .appendPath("_findByUnique")
                .addParam("unique", unique)
                .build();

        return client.doPost(uri, client.factory().option(Issue.class));
    }

    private String createIssueOrGetUnique(final IssueCreate issueCreate) {
        try {
            return session.issues().create(issueCreate).getKey();
        } catch (StartrekClientException exception) {
            if (exception.getErrors().getStatusCode() == HttpStatus.SC_CONFLICT) {
                final Option<Object> unique = issueCreate.getValues().getO("unique");
                if (unique.isPresent()) {
                    final Option<Issue> issue = findByUnique(unique.get());
                    if (issue.isPresent()) {
                        return issue.get().getKey();
                    }
                }
            }
            throw exception;
        }

    }

    private <T> T withHealthyReport(final Supplier<T> producer) {
        boolean success = false;
        Stopwatch stopwatch = Stopwatch.createStarted();
        try {
            final T result = producer.get();
            success = true;
            return result;
        } finally {
            totalRate.inc();
            if (!success) {
                errorRate.inc();
            }
            elapsedTime.record(stopwatch.elapsed(TimeUnit.MILLISECONDS));
        }
    }

    @Override
    public void executeTransition(final String issueKey, final String targetStatusKey, final IssueUpdate issueUpdate) {
        final String transitionId = getTransitionId(issueKey, targetStatusKey);
        withHealthyReport(() -> session.transitions().execute(issueKey, transitionId, issueUpdate));
    }

    @Override
    public void updateIssue(final String issueKey, final IssueUpdate issueUpdate) {
        withHealthyReport(() -> session.issues().update(issueKey, issueUpdate));
    }

    private String getTransitionId(final String issueKey, final String targetStatusKey) {
        final ListF<Transition> transitions = withHealthyReport(() -> session.transitions().getAll(issueKey));

        for (final Transition transition : transitions) {
            if (targetStatusKey.equals(transition.getTo().getKey())) {
                return transition.getId();
            }
        }
        throw new IllegalArgumentException("Unavailable status '" + targetStatusKey + "' for issue '" + issueKey + "'");
    }

    @NotNull
    @Override
    public Issue getIssue(final String issueKey) {
        return session.issues().get(issueKey);
    }

    @Override
    public void createComment(final String issueKey, final CommentCreate comment) {
        withHealthyReport(() -> session.comments().create(issueKey, comment));
    }

    @TestOnly
    public IteratorF<Comment> getComments(final String issueKey) {
        return session.comments().getAll(issueKey);
    }
}
