package ru.yandex.qe.dispenser.client.v1.impl;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.apache.cxf.jaxrs.client.WebClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.util.JsonSerializerBase;
import ru.yandex.qe.dispenser.api.v1.DiPerformer;
import ru.yandex.qe.dispenser.api.v1.DiQuotaChangeCause;
import ru.yandex.qe.dispenser.api.v1.request.DiActionType;
import ru.yandex.qe.dispenser.api.v1.request.DiEntity;
import ru.yandex.qe.dispenser.api.v1.request.DiOperation;
import ru.yandex.qe.dispenser.api.v1.request.DiProcessingMode;
import ru.yandex.qe.dispenser.api.v1.response.DiListResponse;
import ru.yandex.qe.dispenser.api.v1.response.DiOperationResult;
import ru.yandex.qe.dispenser.api.v1.response.DiQuotaChangeResponse;
import ru.yandex.qe.dispenser.client.v1.builder.BatchWeakQuotaChangeRequestBuilder;

public class BatchWeakQuotaChangeRequestBuilderImpl implements BatchWeakQuotaChangeRequestBuilder {
    @NotNull
    private final BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody body;

    @NotNull
    private final Supplier<WebClient> clients;
    private final String entitySpecKey;
    private final String serviceKey;

    BatchWeakQuotaChangeRequestBuilderImpl(@NotNull final String serviceKey,
                                           @NotNull final String entitySpecKey,
                                           @NotNull final Supplier<WebClient> clients) {
        this.clients = clients;
        this.entitySpecKey = entitySpecKey;
        this.serviceKey = serviceKey;
        this.body = new BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody(DiProcessingMode.ROLLBACK_ON_ERROR, serviceKey);
    }


    @NotNull
    @Override
    public DiQuotaChangeResponse perform() {
        final BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeResponseBody response = clients.get()
                .path(String.format("/%s/%s/change-quotas", serviceKey, entitySpecKey))
                .query("reqId", WebClientUtils.actualReqId("fake-req-id"))
                .header("X-Req-Id", WebClientUtils.actualReqId("fake-req-id"))
                .post(body, BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeResponseBody.class);
        final List<DiOperationResult> operationResults = response.stream()
                .map(r -> new DiOperationResult(body.operations.get(r.operationId), r.isSuccess, r.errorReason))
                .collect(Collectors.toList());
        return new DiQuotaChangeResponse(operationResults);
    }

    @Override
    public @NotNull BatchWeakQuotaChangeRequestBuilder createEntity(final DiEntity.@NotNull Builder entity,
                                                                    final @NotNull DiPerformer performer) {
        return createEntity(entity, performer, DiQuotaChangeCause.createEntity(makeEntity(entity), performer).build());
    }

    @Override
    public @NotNull BatchWeakQuotaChangeRequestBuilder createEntity(final DiEntity.@NotNull Builder entity,
                                                                    @NotNull final DiPerformer performer,
                                                                    @NotNull final DiQuotaChangeCause cause) {
        return addOperation(new DiOperation<>(DiActionType.CREATE_ENTITY, makeEntity(entity), performer, cause));
    }

    @NotNull
    @Override
    public BatchWeakQuotaChangeRequestBuilder releaseEntity(@NotNull final DiEntity.Builder entity) {
        return releaseEntity(entity, DiQuotaChangeCause.releaseEntity(makeEntity(entity)).build());
    }

    @NotNull
    @Override
    public BatchWeakQuotaChangeRequestBuilder releaseEntity(@NotNull final DiEntity.Builder entity, @NotNull final DiQuotaChangeCause cause) {
        return addOperation(new DiOperation<>(DiActionType.RELEASE_ENTITY, makeEntity(entity), null, cause));
    }

    @NotNull
    private BatchWeakQuotaChangeRequestBuilder addOperation(@NotNull final DiOperation<?> operation) {
        body.add(operation);
        return this;
    }

    private DiEntity makeEntity(final DiEntity.Builder builder) {
        if (builder.specificationKey != null && !entitySpecKey.equals(builder.specificationKey)) {
            throw new IllegalStateException(String.format("specificationKey missmatch %s expected, got %s", entitySpecKey, builder.specificationKey));
        }
        builder.bySpecification(entitySpecKey);
        return builder.build();
    }

    @JsonSerialize(using = BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody.Serializer.class)
    static final class BatchChangeRequestBody {
        @NotNull
        private final DiProcessingMode mode;
        @NotNull
        private final String serviceKey;
        @NotNull
        private final Map<Integer, DiOperation<?>> operations = new HashMap<>();

        @NotNull
        private final AtomicInteger operationIdGenerator = new AtomicInteger(0);

        BatchChangeRequestBody(@NotNull final DiProcessingMode mode, @NotNull final String serviceKey) {
            this.mode = mode;
            this.serviceKey = serviceKey;
        }

        void add(@NotNull final DiOperation<?> operation) {
            operations.put(operationIdGenerator.incrementAndGet(), operation);
        }

        int size() {
            return operations.size();
        }

        @Override
        public boolean equals(@Nullable final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody that = (BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody) o;
            return mode == that.mode && serviceKey.equals(that.serviceKey) && operations.equals(that.operations);
        }

        @Override
        public int hashCode() {
            return 31 * (31 * mode.hashCode() + serviceKey.hashCode()) + operations.hashCode();
        }

        static class Serializer extends JsonSerializerBase<BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody> {
            @Override
            public void serialize(@NotNull final BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeRequestBody value,
                                  @NotNull final JsonGenerator jg,
                                  @NotNull final SerializerProvider sp) throws IOException {
                jg.writeStartObject();
                jg.writeObjectField("mode", value.mode);
                jg.writeObjectField("serviceKey", value.serviceKey);
                jg.writeArrayFieldStart("operations");
                for (final Map.Entry<Integer, DiOperation<?>> operation : value.operations.entrySet()) {
                    jg.writeStartObject();
                    jg.writeObjectField("id", operation.getKey());
                    jg.writeObjectField("operation", operation.getValue());
                    jg.writeEndObject();
                }
                jg.writeEndArray();
                jg.writeEndObject();
            }
        }
    }

    static final class BatchChangeResponseBody extends
            DiListResponse<BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeResponseBody.OperationResultReference> {
        BatchChangeResponseBody(@JsonProperty("result") final Collection<BatchWeakQuotaChangeRequestBuilderImpl.BatchChangeResponseBody.OperationResultReference> result) {
            super(result);
        }

        static final class OperationResultReference {
            private final int operationId;
            private final boolean isSuccess;
            @Nullable
            private final String errorReason;

            OperationResultReference(@JsonProperty("operationId") final int operationId,
                                     @JsonProperty("isSuccess") final boolean isSuccess,
                                     @JsonProperty("errorReason") @Nullable final String errorReason) {
                this.operationId = operationId;
                this.isSuccess = isSuccess;
                this.errorReason = errorReason;
            }
        }
    }
}
