package com.hubspot.baragon.utils;

import com.google.common.base.Function;
import com.google.inject.Inject;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.utils.ZKPaths;
import org.apache.log4j.Logger;
import org.apache.zookeeper.KeeperException;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ZkParallelFetcher {
  private static final Logger LOG = Logger.getLogger(ZkParallelFetcher.class);
  private static final int TIMEOUT_SECONDS = 10;

  private final CuratorFramework curatorFramework;

  @Inject
  public ZkParallelFetcher(CuratorFramework framework) {
    this.curatorFramework = framework;
  }

  public <T> Map<String, T> fetchDataInParallel(Collection<String> paths, Function<byte[], T> transformFunction) throws Exception {
    Map<String, T> dataMap = new ConcurrentHashMap<>();
    CountDownLatch countDownLatch = new CountDownLatch(paths.size());
    Queue<KeeperException> exceptions = new ConcurrentLinkedQueue<>();
    BackgroundCallback callback = new GetDataCallback<>(dataMap, transformFunction, countDownLatch, exceptions);

    for (String path : paths) {
      curatorFramework.getData().inBackground(callback).forPath(path);
    }

    if (!countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
      throw new TimeoutException("ZkChildrenFetcher timed out waiting for data");
    }

    for (KeeperException exception : exceptions) {
      LOG.error(exception);
    }

    if (!exceptions.isEmpty()) {
      throw exceptions.peek();
    }

    return dataMap;
  }

  public Map<String, Collection<String>> fetchChildrenInParallel(Collection<String> paths) throws Exception {
    // Didn't use Guava Multimap because we need thread-safety
    Map<String, Collection<String>> childMap = new ConcurrentHashMap<>();
    CountDownLatch countDownLatch = new CountDownLatch(paths.size());
    Queue<KeeperException> exceptions = new ConcurrentLinkedQueue<>();
    BackgroundCallback callback = new GetChildrenCallback(childMap, countDownLatch, exceptions);

    for (String path : paths) {
      curatorFramework.getChildren().inBackground(callback).forPath(path);
    }

    if (!countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
      throw new TimeoutException("ZkChildrenFetcher timed out waiting for data");
    }

    for (KeeperException exception : exceptions) {
      LOG.error(exception);
    }

    if (!exceptions.isEmpty()) {
      throw exceptions.peek();
    }

    return childMap;
  }

  private class GetDataCallback<T> implements BackgroundCallback {
    private final Map<String, T> dataMap;
    private final Function<byte[], T> transformFunction;
    private final CountDownLatch countDownLatch;
    private final Queue<KeeperException> exceptions;

    private GetDataCallback(Map<String, T> dataMap,
                            Function<byte[], T> transformFunction,
                            CountDownLatch countDownLatch,
                            Queue<KeeperException> exceptions) {
      this.dataMap = dataMap;
      this.transformFunction = transformFunction;
      this.countDownLatch = countDownLatch;
      this.exceptions = exceptions;
    }

    @Override
    public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
      try {
        KeeperException.Code code = KeeperException.Code.get(event.getResultCode());

        switch (code) {
          case OK:
            T data = event.getData() == null ? null : transformFunction.apply(event.getData());
            dataMap.put(ZKPaths.getNodeFromPath(event.getPath()), data);
            break;
          case NONODE:
            // In this case there was a race condition in which the child node was deleted before we asked for data.
            break;
          default:
            exceptions.add(KeeperException.create(code, event.getPath()));
        }
      } finally {
        countDownLatch.countDown();
      }
    }
  }

  private class GetChildrenCallback implements BackgroundCallback {
    private final Map<String, Collection<String>> childMap;
    private final CountDownLatch countDownLatch;
    private final Queue<KeeperException> exceptions;

    private GetChildrenCallback(Map<String, Collection<String>> childMap,
                                CountDownLatch countDownLatch,
                                Queue<KeeperException> exceptions) {
      this.childMap = childMap;
      this.countDownLatch = countDownLatch;
      this.exceptions = exceptions;
    }

    @Override
    public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
      try {
        KeeperException.Code code = KeeperException.Code.get(event.getResultCode());

        switch (code) {
          case OK:
            childMap.put(ZKPaths.getNodeFromPath(event.getPath()), new HashSet<>(event.getChildren()));
            break;
          case NONODE:
            // In this case there was a race condition in which the child node was deleted before we asked for data.
            break;
          default:
            exceptions.add(KeeperException.create(code, event.getPath()));
        }
      } finally {
        countDownLatch.countDown();
      }
    }
  }
}

@@ -1603,317 +1603,59 @@
     
-if (!countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) %7B%0A      throw new TimeoutException(%22ZkChildrenFetcher timed out waiting for data%22);%0A    %7D%0A%0A    for (KeeperException exception : exceptions) %7B%0A      LOG.error(exception);%0A    %7D%0A%0A    if (!exceptions.isEmpty()) %7B%0A      throw exceptions.peek();%0A    %7D%0A
+waitAndThrowExceptions(countDownLatch, exceptions);
 %0A   
@@ -2268,32 +2268,237 @@
 h(path);%0A    %7D%0A%0A
+    waitAndThrowExceptions(countDownLatch, exceptions);%0A    return childMap;%0A  %7D%0A%0A  private void waitAndThrowExceptions(CountDownLatch countDownLatch, Queue%3CKeeperException%3E exceptions) throws Exception %7B%0A
     if (!countDo
@@ -2792,35 +2792,13 @@
 ();%0A
+
     %7D
-%0A%0A    return childMap;
 %0A  %7D

Extract common handling