/*
 * The MIT License (MIT)
 * Copyright (c) 2018 Microsoft Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.microsoft.azure.cosmosdb.rx.examples.multimaster.samples;

import com.microsoft.azure.cosmosdb.AccessCondition;
import com.microsoft.azure.cosmosdb.AccessConditionType;
import com.microsoft.azure.cosmosdb.Conflict;
import com.microsoft.azure.cosmosdb.ConflictResolutionMode;
import com.microsoft.azure.cosmosdb.ConflictResolutionPolicy;
import com.microsoft.azure.cosmosdb.Document;
import com.microsoft.azure.cosmosdb.DocumentClientException;
import com.microsoft.azure.cosmosdb.DocumentCollection;
import com.microsoft.azure.cosmosdb.FeedResponse;
import com.microsoft.azure.cosmosdb.RequestOptions;
import com.microsoft.azure.cosmosdb.Resource;
import com.microsoft.azure.cosmosdb.ResourceResponse;
import com.microsoft.azure.cosmosdb.StoredProcedure;
import com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient;
import com.microsoft.azure.cosmosdb.rx.examples.multimaster.Helpers;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Scheduler;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConflictWorker {
    private static Logger logger = LoggerFactory.getLogger(ConflictWorker.class);

    private final Scheduler schedulerForBlockingWork;
    private final List<AsyncDocumentClient> clients;
    private final String basicCollectionUri;
    private final String manualCollectionUri;
    private final String lwwCollectionUri;
    private final String udpCollectionUri;
    private final String databaseName;
    private final String basicCollectionName;
    private final String manualCollectionName;
    private final String lwwCollectionName;
    private final String udpCollectionName;
    private final ExecutorService executor;

    public ConflictWorker(String databaseName, String basicCollectionName, String manualCollectionName, String lwwCollectionName, String udpCollectionName) {
        this.clients = new ArrayList<>();
        this.basicCollectionUri = Helpers.createDocumentCollectionUri(databaseName, basicCollectionName);
        this.manualCollectionUri = Helpers.createDocumentCollectionUri(databaseName, manualCollectionName);
        this.lwwCollectionUri = Helpers.createDocumentCollectionUri(databaseName, lwwCollectionName);
        this.udpCollectionUri = Helpers.createDocumentCollectionUri(databaseName, udpCollectionName);

        this.databaseName = databaseName;
        this.basicCollectionName = basicCollectionName;
        this.manualCollectionName = manualCollectionName;
        this.lwwCollectionName = lwwCollectionName;
        this.udpCollectionName = udpCollectionName;

        this.executor = Executors.newFixedThreadPool(100);
        this.schedulerForBlockingWork = Schedulers.from(executor);
    }

    public void addClient(AsyncDocumentClient client) {
        this.clients.add(client);
    }

    private DocumentCollection createCollectionIfNotExists(AsyncDocumentClient createClient, String databaseName, DocumentCollection collection) {
        return Helpers.createCollectionIfNotExists(createClient, this.databaseName, collection)
                .subscribeOn(schedulerForBlockingWork).toBlocking().value();
    }

    private DocumentCollection createCollectionIfNotExists(AsyncDocumentClient createClient, String databaseName, String collectionName) {

        return Helpers.createCollectionIfNotExists(createClient, this.databaseName, this.basicCollectionName)
                .subscribeOn(schedulerForBlockingWork).toBlocking().value();
    }

    private DocumentCollection getCollectionDefForManual(String id) {
        DocumentCollection collection = new DocumentCollection();
        collection.setId(id);
        ConflictResolutionPolicy policy = ConflictResolutionPolicy.createCustomPolicy();
        collection.setConflictResolutionPolicy(policy);
        return collection;
    }

    private DocumentCollection getCollectionDefForLastWinWrites(String id, String conflictResolutionPath) {
        DocumentCollection collection = new DocumentCollection();
        collection.setId(id);
        ConflictResolutionPolicy policy = ConflictResolutionPolicy.createLastWriterWinsPolicy(conflictResolutionPath);
        collection.setConflictResolutionPolicy(policy);
        return collection;
    }

    private DocumentCollection getCollectionDefForCustom(String id, String storedProc) {
        DocumentCollection collection = new DocumentCollection();
        collection.setId(id);
        ConflictResolutionPolicy policy = ConflictResolutionPolicy.createCustomPolicy(storedProc);
        collection.setConflictResolutionPolicy(policy);
        return collection;
    }

    public void initialize() throws Exception {
        AsyncDocumentClient createClient = this.clients.get(0);

        Helpers.createDatabaseIfNotExists(createClient, this.databaseName).subscribeOn(schedulerForBlockingWork).toBlocking().value();

        DocumentCollection basic = createCollectionIfNotExists(createClient, this.databaseName, this.basicCollectionName);

        DocumentCollection manualCollection = createCollectionIfNotExists(createClient,
                Helpers.createDatabaseUri(this.databaseName), getCollectionDefForManual(this.manualCollectionName));

        DocumentCollection lwwCollection = createCollectionIfNotExists(createClient,
                Helpers.createDatabaseUri(this.databaseName), getCollectionDefForLastWinWrites(this.lwwCollectionName, "/regionId"));

        DocumentCollection udpCollection = createCollectionIfNotExists(createClient,
                Helpers.createDatabaseUri(this.databaseName), getCollectionDefForCustom(this.udpCollectionName,
                        String.format("dbs/%s/colls/%s/sprocs/%s", this.databaseName, this.udpCollectionName, "resolver")));

        StoredProcedure lwwSproc = new StoredProcedure();
        lwwSproc.setId("resolver");
        lwwSproc.setBody(IOUtils.toString(
                getClass().getClassLoader().getResourceAsStream("resolver-storedproc.txt"), "UTF-8"));

        lwwSproc =
                getResource(createClient.upsertStoredProcedure(
                        Helpers.createDocumentCollectionUri(this.databaseName, this.udpCollectionName), lwwSproc, null));

    }

    private <T extends Resource> T getResource(Observable<ResourceResponse<T>> obs) {
        return obs.subscribeOn(schedulerForBlockingWork).toBlocking().single().getResource();
    }

    public void runManualConflict() throws Exception {
        logger.info("\r\nInsert Conflict\r\n");
        this.runInsertConflictOnManual();

        logger.info("\r\nUpdate Conflict\r\n");
        this.runUpdateConflictOnManual();

        logger.info("\r\nDelete Conflict\r\n");
        this.runDeleteConflictOnManual();
    }

    public void runLWWConflict() throws Exception {
        logger.info("\r\nInsert Conflict\r\n");
        this.runInsertConflictOnLWW();

        logger.info("\r\nUpdate Conflict\r\n");
        this.runUpdateConflictOnLWW();

        logger.info("\r\nDelete Conflict\r\n");
        this.runDeleteConflictOnLWW();
    }

    public void runUDPConflict() throws Exception {
        logger.info("\r\nInsert Conflict\r\n");
        this.runInsertConflictOnUdp();

        logger.info("\r\nUpdate Conflict\r\n");
        this.runUpdateConflictOnUdp();

        logger.info("\r\nDelete Conflict\r\n");
        this.runDeleteConflictOnUdp();
    }

    public void runInsertConflictOnManual() throws Exception {
        do {
            logger.info("1) Performing conflicting insert across {} regions on {}", this.clients.size(), this.manualCollectionName);

            ArrayList<Observable<Document>> insertTask = new ArrayList<Observable<Document>>();

            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                insertTask.add(this.tryInsertDocument(client, this.manualCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(insertTask).toList().subscribeOn(schedulerForBlockingWork).toBlocking().single();

            if (conflictDocuments.size() == this.clients.size()) {
                logger.info("2) Caused {} insert conflicts, verifying conflict resolution", conflictDocuments.size());

                for (Document conflictingInsert : conflictDocuments) {
                    this.validateManualConflict(this.clients, conflictingInsert);
                }
                break;
            } else {
                logger.info("Retrying insert to induce conflicts");
            }
        } while (true);
    }

    public void runUpdateConflictOnManual() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());


            conflictDocument = this.tryInsertDocument(clients.get(0), this.manualCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();

            TimeUnit.SECONDS.sleep(1);//1 Second for write to sync.


            logger.info("1) Performing conflicting update across 3 regions on {}", this.manualCollectionName);

            ArrayList<Observable<Document>> updateTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                updateTask.add(this.tryUpdateDocument(client, this.manualCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(updateTask).toList().toBlocking().single();

            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} updated conflicts, verifying conflict resolution", conflictDocuments.size());

                for (Document conflictingUpdate : conflictDocuments) {
                    this.validateManualConflict(this.clients, conflictingUpdate);
                }
                break;
            } else {
                logger.info("Retrying update to induce conflicts");
            }
        } while (true);
    }

    public void runDeleteConflictOnManual() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            conflictDocument = this.tryInsertDocument(clients.get(0), this.manualCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();

            TimeUnit.SECONDS.sleep(10);//1 Second for write to sync.

            logger.info("1) Performing conflicting delete across 3 regions on {}", this.manualCollectionName);

            ArrayList<Observable<Document>> deleteTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                deleteTask.add(this.tryDeleteDocument(client, this.manualCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(deleteTask).toList()
                    .subscribeOn(schedulerForBlockingWork)
                    .toBlocking().single();

            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} delete conflicts, verifying conflict resolution", conflictDocuments.size());

                for (Document conflictingDelete : conflictDocuments) {
                    this.validateManualConflict(this.clients, conflictingDelete);
                }

                break;
            } else {
                logger.info("Retrying update to induce conflicts");
            }
        } while (true);
    }

    public void runInsertConflictOnLWW() throws Exception {
        do {
            logger.info("Performing conflicting insert across 3 regions");

            ArrayList<Observable<Document>> insertTask = new ArrayList<Observable<Document>>();

            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                insertTask.add(this.tryInsertDocument(client, this.lwwCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(insertTask).toList().toBlocking().single();


            if (conflictDocuments.size() > 1) {
                logger.info("Inserted {} conflicts, verifying conflict resolution", conflictDocuments.size());

                this.validateLWW(this.clients, conflictDocuments);

                break;
            } else {
                logger.info("Retrying insert to induce conflicts");
            }
        } while (true);
    }

    public void runUpdateConflictOnLWW() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            conflictDocument = this.tryInsertDocument(clients.get(0), this.lwwCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();


            TimeUnit.SECONDS.sleep(1); //1 Second for write to sync.

            logger.info("1) Performing conflicting update across {} regions on {}", this.clients.size(), this.lwwCollectionUri);

            ArrayList<Observable<Document>> insertTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                insertTask.add(this.tryUpdateDocument(client, this.lwwCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(insertTask).toList().toBlocking().single();


            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} update conflicts, verifying conflict resolution", conflictDocuments.size());

                this.validateLWW(this.clients, conflictDocuments);

                break;
            } else {
                logger.info("Retrying insert to induce conflicts");
            }
        } while (true);
    }

    public void runDeleteConflictOnLWW() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            conflictDocument = this.tryInsertDocument(clients.get(0), this.lwwCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();


            TimeUnit.SECONDS.sleep(1); //1 Second for write to sync.

            logger.info("1) Performing conflicting delete across {} regions on {}", this.clients.size(), this.lwwCollectionUri);

            ArrayList<Observable<Document>> insertTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                if (index % 2 == 1) {
                    //We delete from region 1, even though region 2 always win.
                    insertTask.add(this.tryDeleteDocument(client, this.lwwCollectionUri, conflictDocument, index++));
                } else {
                    insertTask.add(this.tryUpdateDocument(client, this.lwwCollectionUri, conflictDocument, index++));
                }
            }

            List<Document> conflictDocuments = Observable.merge(insertTask).toList().toBlocking().single();

            if (conflictDocuments.size() > 1) {
                logger.info("Inserted {} conflicts, verifying conflict resolution", conflictDocuments.size());

                //Delete should always win. irrespective of LWW.
                this.validateLWW(this.clients, conflictDocuments, true);
                break;
            } else {
                logger.info("Retrying update/delete to induce conflicts");
            }
        } while (true);
    }

    public void runInsertConflictOnUdp() throws Exception {
        do {
            logger.info("1) Performing conflicting insert across 3 regions on {}", this.udpCollectionName);

            ArrayList<Observable<Document>> insertTask = new ArrayList<Observable<Document>>();

            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                insertTask.add(this.tryInsertDocument(client, this.udpCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(insertTask).toList().toBlocking().single();


            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} insert conflicts, verifying conflict resolution", conflictDocuments.size());

                this.validateUDPAsync(this.clients, conflictDocuments);

                break;
            } else {
                logger.info("Retrying insert to induce conflicts");
            }
        } while (true);
    }

    public void runUpdateConflictOnUdp() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            conflictDocument = this.tryInsertDocument(clients.get(0), this.udpCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();

            TimeUnit.SECONDS.sleep(1); //1 Second for write to sync.

            logger.info("1) Performing conflicting update across 3 regions on {}", this.udpCollectionUri);

            ArrayList<Observable<Document>> updateTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                updateTask.add(this.tryUpdateDocument(client, this.udpCollectionUri, conflictDocument, index++));
            }

            List<Document> conflictDocuments = Observable.merge(updateTask).toList().toBlocking().single();


            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} update conflicts, verifying conflict resolution", conflictDocuments.size());

                this.validateUDPAsync(this.clients, conflictDocuments);

                break;
            } else {
                logger.info("Retrying update to induce conflicts");
            }
        } while (true);
    }

    public void runDeleteConflictOnUdp() throws Exception {
        do {
            Document conflictDocument = new Document();
            conflictDocument.setId(UUID.randomUUID().toString());

            conflictDocument = this.tryInsertDocument(clients.get(0), this.udpCollectionUri, conflictDocument, 0)
                    .firstOrDefault(null).toBlocking().first();

            TimeUnit.SECONDS.sleep(1); //1 Second for write to sync.

            logger.info("1) Performing conflicting update/delete across 3 regions on {}", this.udpCollectionUri);

            ArrayList<Observable<Document>> deleteTask = new ArrayList<Observable<Document>>();

            int index = 0;
            for (AsyncDocumentClient client : this.clients) {
                if (index % 2 == 1) {
                    //We delete from region 1, even though region 2 always win.
                    deleteTask.add(this.tryDeleteDocument(client, this.udpCollectionUri, conflictDocument, index++));
                } else {
                    deleteTask.add(this.tryUpdateDocument(client, this.udpCollectionUri, conflictDocument, index++));
                }
            }

            List<Document> conflictDocuments = Observable.merge(deleteTask).toList().toBlocking().single();

            if (conflictDocuments.size() > 1) {
                logger.info("2) Caused {} delete conflicts, verifying conflict resolution", conflictDocuments.size());

                //Delete should always win. irrespective of LWW.
                this.validateUDPAsync(this.clients, conflictDocuments, true);
                break;
            } else {
                logger.info("Retrying update/delete to induce conflicts");
            }
        } while (true);
    }

    private Observable<Document> tryInsertDocument(AsyncDocumentClient client, String collectionUri, Document document, int index) {

        logger.debug("region: {}", client.getWriteEndpoint());
        document.set("regionId", index);
        document.set("regionEndpoint", client.getReadEndpoint());
        return client.createDocument(collectionUri, document, null, false)
                .onErrorResumeNext(e -> {
                    if (hasDocumentClientException(e, 409)) {
                        return Observable.empty();
                    } else {
                        return Observable.error(e);
                    }
                }).map(ResourceResponse::getResource);
    }

    private boolean hasDocumentClientException(Throwable e, int statusCode) {
        if (e instanceof DocumentClientException) {
            DocumentClientException dce = (DocumentClientException) e;
            return dce.getStatusCode() == statusCode;
        }

        return false;
    }

    private boolean hasDocumentClientExceptionCause(Throwable e) {
        while (e != null) {
            if (e instanceof DocumentClientException) {
                return true;
            }

            e = e.getCause();
        }
        return false;
    }

    private boolean hasDocumentClientExceptionCause(Throwable e, int statusCode) {
        while (e != null) {
            if (e instanceof DocumentClientException) {
                DocumentClientException dce = (DocumentClientException) e;
                return dce.getStatusCode() == statusCode;
            }

            e = e.getCause();
        }

        return false;
    }

    private Observable<Document> tryUpdateDocument(AsyncDocumentClient client, String collectionUri, Document document, int index) {
        document.set("regionId", index);
        document.set("regionEndpoint", client.getReadEndpoint());

        RequestOptions options = new RequestOptions();
        options.setAccessCondition(new AccessCondition());
        options.getAccessCondition().setType(AccessConditionType.IfMatch);
        options.getAccessCondition().setCondition(document.getETag());


        return client.replaceDocument(document.getSelfLink(), document, null).onErrorResumeNext(e -> {

            // pre condition failed
            if (hasDocumentClientException(e, 412)) {
                //Lost synchronously or not document yet. No conflict is induced.
                return Observable.empty();

            }
            return Observable.error(e);
        }).map(ResourceResponse::getResource);
    }

    private Observable<Document> tryDeleteDocument(AsyncDocumentClient client, String collectionUri, Document document, int index) {
        document.set("regionId", index);
        document.set("regionEndpoint", client.getReadEndpoint());

        RequestOptions options = new RequestOptions();
        options.setAccessCondition(new AccessCondition());
        options.getAccessCondition().setType(AccessConditionType.IfMatch);
        options.getAccessCondition().setCondition(document.getETag());


        return client.deleteDocument(document.getSelfLink(), options).onErrorResumeNext(e -> {

            // pre condition failed
            if (hasDocumentClientException(e, 412)) {
                //Lost synchronously. No conflict is induced.
                return Observable.empty();

            }
            return Observable.error(e);
        }).map(rr -> document);
    }

    private void validateManualConflict(List<AsyncDocumentClient> clients, Document conflictDocument) throws Exception {
        boolean conflictExists = false;
        for (AsyncDocumentClient client : clients) {
            conflictExists = this.validateManualConflict(client, conflictDocument);
        }

        if (conflictExists) {
            this.deleteConflict(conflictDocument);
        }
    }

    private boolean isDelete(Conflict conflict) {
        return StringUtils.equalsIgnoreCase(conflict.getOperationKind(), "delete");
    }


    private boolean equals(String a, String b) {
        return StringUtils.equals(a, b);
    }

    private boolean validateManualConflict(AsyncDocumentClient client, Document conflictDocument) throws Exception {
        while (true) {
            FeedResponse<Conflict> response = client.readConflicts(this.manualCollectionUri, null)
                    .first().toBlocking().single();

            for (Conflict conflict : response.getResults()) {
                if (!isDelete(conflict)) {
                    Document conflictDocumentContent = conflict.getResource(Document.class);
                    if (equals(conflictDocument.getId(), conflictDocumentContent.getId())) {
                        if (equals(conflictDocument.getResourceId(), conflictDocumentContent.getResourceId()) &&
                                equals(conflictDocument.getETag(), conflictDocumentContent.getETag())) {
                            logger.info("Document from Region {} lost conflict @ {}",
                                    conflictDocument.getId(),
                                    conflictDocument.getInt("regionId"),
                                    client.getReadEndpoint());
                            return true;
                        } else {
                            try {
                                //Checking whether this is the winner.
                                Document winnerDocument = client.readDocument(conflictDocument.getSelfLink(), null)
                                        .toBlocking().single().getResource();
                                logger.info("Document from region {} won the conflict @ {}",
                                        conflictDocument.getInt("regionId"),
                                        client.getReadEndpoint());
                                return false;
                            }
                            catch (Exception exception) {
                                if (hasDocumentClientException(exception, 404)) {
                                    throw exception;
                                } else {
                                    logger.info(
                                            "Document from region {} not found @ {}",
                                            conflictDocument.getInt("regionId"),
                                            client.getReadEndpoint());
                                }
                            }
                        }
                    }
                } else {
                    if (equals(conflict.getSourceResourceId(), conflictDocument.getResourceId())) {
                        logger.info("Delete conflict found @ {}",
                                client.getReadEndpoint());
                        return false;
                    }
                }
            }

            logger.error("Document {} is not found in conflict feed @ {}, retrying",
                    conflictDocument.getId(),
                    client.getReadEndpoint());

            TimeUnit.MILLISECONDS.sleep(500);
        }
    }

    private void deleteConflict(Document conflictDocument) {
        AsyncDocumentClient delClient = clients.get(0);

        FeedResponse<Conflict> conflicts = delClient.readConflicts(this.manualCollectionUri, null).first().toBlocking().single();

        for (Conflict conflict : conflicts.getResults()) {
            if (!isDelete(conflict)) {
                Document conflictContent = conflict.getResource(Document.class);
                if (equals(conflictContent.getResourceId(), conflictDocument.getResourceId())
                        && equals(conflictContent.getETag(), conflictDocument.getETag())) {
                    logger.info("Deleting manual conflict {} from region {}",
                            conflict.getSourceResourceId(),
                            conflictContent.getInt("regionId"));
                    delClient.deleteConflict(conflict.getSelfLink(), null)
                            .toBlocking().single();

                }
            } else if (equals(conflict.getSourceResourceId(), conflictDocument.getResourceId())) {
                logger.info("Deleting manual conflict {} from region {}",
                        conflict.getSourceResourceId(),
                        conflictDocument.getInt("regionId"));
                delClient.deleteConflict(conflict.getSelfLink(), null)
                        .toBlocking().single();
            }
        }
    }

    private void validateLWW(List<AsyncDocumentClient> clients, List<Document> conflictDocument) throws Exception {
        validateLWW(clients, conflictDocument, false);
    }


    private void validateLWW(List<AsyncDocumentClient> clients, List<Document> conflictDocument, boolean hasDeleteConflict) throws Exception {
        for (AsyncDocumentClient client : clients) {
            this.validateLWW(client, conflictDocument, hasDeleteConflict);
        }
    }

    private void validateLWW(AsyncDocumentClient client, List<Document> conflictDocument, boolean hasDeleteConflict) throws Exception {
        FeedResponse<Conflict> response = client.readConflicts(this.lwwCollectionUri, null)
                .first().toBlocking().single();

        if (response.getResults().size() != 0) {
            logger.error("Found {} conflicts in the lww collection", response.getResults().size());
            return;
        }

        if (hasDeleteConflict) {
            do {
                try {
                    client.readDocument(conflictDocument.get(0).getSelfLink(), null).toBlocking().single();

                    logger.error("Delete conflict for document {} didnt win @ {}",
                            conflictDocument.get(0).getId(),
                            client.getReadEndpoint());

                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (Exception exception) {
                    if (!hasDocumentClientExceptionCause(exception)) {
                        throw exception;
                    }

                    // NotFound
                    if (hasDocumentClientExceptionCause(exception, 404)) {

                        logger.info("Delete conflict won @ {}", client.getReadEndpoint());
                        return;
                    } else {
                        logger.error("Delete conflict for document {} didnt win @ {}",
                                conflictDocument.get(0).getId(),
                                client.getReadEndpoint());

                        TimeUnit.MILLISECONDS.sleep(500);
                    }
                }
            } while (true);
        }

        Document winnerDocument = null;

        for (Document document : conflictDocument) {
            if (winnerDocument == null ||
                    winnerDocument.getInt("regionId") <= document.getInt("regionId")) {
                winnerDocument = document;
            }
        }

        logger.info("Document from region {} should be the winner",
                winnerDocument.getInt("regionId"));

        while (true) {
            try {
                Document existingDocument = client.readDocument(winnerDocument.getSelfLink(), null)
                        .toBlocking().single().getResource();

                if (existingDocument.getInt("regionId") == winnerDocument.getInt("regionId")) {
                    logger.info("Winner document from region {} found at {}",
                            existingDocument.getInt("regionId"),
                            client.getReadEndpoint());
                    break;
                } else {
                    logger.error("Winning document version from region {} is not found @ {}, retrying...",
                            winnerDocument.getInt("regionId"),
                            client.getWriteEndpoint());
                    TimeUnit.MILLISECONDS.sleep(500);
                }
            } catch (Exception e) {
                logger.error("Winner document from region {} is not found @ {}, retrying...",
                        winnerDocument.getInt("regionId"),
                        client.getWriteEndpoint());
                TimeUnit.MILLISECONDS.sleep(500);
            }
        }
    }

    private void validateUDPAsync(List<AsyncDocumentClient> clients, List<Document> conflictDocument) throws Exception {
        validateUDPAsync(clients, conflictDocument, false);
    }

    private void validateUDPAsync(List<AsyncDocumentClient> clients, List<Document> conflictDocument, boolean hasDeleteConflict) throws Exception {
        for (AsyncDocumentClient client : clients) {
            this.validateUDPAsync(client, conflictDocument, hasDeleteConflict);
        }
    }

    private String documentNameLink(String collectionId, String documentId) {
        return String.format("dbs/%s/colls/%s/docs/%s", databaseName, collectionId, documentId);
    }

    private void validateUDPAsync(AsyncDocumentClient client, List<Document> conflictDocument, boolean hasDeleteConflict) throws Exception {
        FeedResponse<Conflict> response = client.readConflicts(this.udpCollectionUri, null).first().toBlocking().single();

        if (response.getResults().size() != 0) {
            logger.error("Found {} conflicts in the udp collection", response.getResults().size());
            return;
        }

        if (hasDeleteConflict) {
            do {
                try {
                    client.readDocument(
                            documentNameLink(udpCollectionName, conflictDocument.get(0).getId()), null)
                            .toBlocking().single();

                    logger.error("Delete conflict for document {} didnt win @ {}",
                            conflictDocument.get(0).getId(),
                            client.getReadEndpoint());

                    TimeUnit.MILLISECONDS.sleep(500);

                } catch (Exception exception) {
                    if (hasDocumentClientExceptionCause(exception, 404)) {
                        logger.info("Delete conflict won @ {}", client.getReadEndpoint());
                        return;
                    } else {
                        logger.error("Delete conflict for document {} didnt win @ {}",
                                conflictDocument.get(0).getId(),
                                client.getReadEndpoint());

                        TimeUnit.MILLISECONDS.sleep(500);
                    }
                }
            } while (true);
        }

        Document winnerDocument = null;

        for (Document document : conflictDocument) {
            if (winnerDocument == null ||
                    winnerDocument.getInt("regionId") <= document.getInt("regionId")) {
                winnerDocument = document;
            }
        }

        logger.info("Document from region {} should be the winner",
                winnerDocument.getInt("regionId"));

        while (true) {
            try {

                Document existingDocument = client.readDocument(
                        documentNameLink(udpCollectionName, winnerDocument.getId()), null)
                        .toBlocking().single().getResource();

                if (existingDocument.getInt("regionId") == winnerDocument.getInt(
                        ("regionId"))) {
                    logger.info("Winner document from region {} found at {}",
                            existingDocument.getInt("regionId"),
                            client.getReadEndpoint());
                    break;
                } else {
                    logger.error("Winning document version from region {} is not found @ {}, retrying...",
                            winnerDocument.getInt("regionId"),
                            client.getWriteEndpoint());
                    TimeUnit.MILLISECONDS.sleep(500);
                }
            } catch (Exception e) {
                logger.error("Winner document from region {} is not found @ {}, retrying...",
                        winnerDocument.getInt("regionId"),
                        client.getWriteEndpoint());
                TimeUnit.MILLISECONDS.sleep(500);
            }
        }
    }

    public void shutdown() {
        this.executor.shutdown();
        for(AsyncDocumentClient client: clients) {
            client.close();
        }
    }
}