package com.hubspot.baragon.data;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.PathAndBytesable;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.io.BaseEncoding;
import com.hubspot.baragon.config.ZooKeeperConfiguration;
import com.hubspot.baragon.utils.JavaUtils;

// because curator is a piece of shit
public abstract class AbstractDataStore {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractDataStore.class);

  public enum OperationType {
    READ,
    WRITE;
  }

  protected final CuratorFramework curatorFramework;
  protected final ObjectMapper objectMapper;
  protected final ZooKeeperConfiguration zooKeeperConfiguration;

  public static final Comparator<String> SEQUENCE_NODE_COMPARATOR_LOW_TO_HIGH = new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
      return o1.substring(o1.length()-10).compareTo(o2.substring(o2.length()-10));
    }
  };

  public static final Comparator<String> SEQUENCE_NODE_COMPARATOR_HIGH_TO_LOW = new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
      return o2.substring(o2.length()-10).compareTo(o1.substring(o1.length()-10));
    }
  };

  public AbstractDataStore(CuratorFramework curatorFramework, ObjectMapper objectMapper, ZooKeeperConfiguration zooKeeperConfiguration) {
    this.curatorFramework = curatorFramework;
    this.objectMapper = objectMapper;
    this.zooKeeperConfiguration = zooKeeperConfiguration;
  }

  protected void log(OperationType type, Optional<Integer> numItems, Optional<Integer> bytes, long start, String path) {
    final String message = String.format("%s (items: %s) (bytes: %s) in %s (%s)", type, numItems.or(1), bytes.or(0), JavaUtils.duration(start), path);

    final long duration = System.currentTimeMillis() - start;

    if ((bytes.isPresent() && bytes.get() > zooKeeperConfiguration.getDebugCuratorCallOverBytes()) || (duration > zooKeeperConfiguration.getDebugCuratorCallOverMillis())) {
      LOG.debug(message);
    } else {
      LOG.trace(message);
    }
  }

  protected String encodeUrl(String url) {
    return BaseEncoding.base64Url().encode(url.getBytes(Charsets.UTF_8));
  }

  protected String decodeUrl(String encodedUrl) {
    return new String(BaseEncoding.base64Url().decode(encodedUrl), Charsets.UTF_8);
  }

  protected String sanitizeNodeName(String name) {
    return name.contains("/") ? encodeUrl(name) : name;
  }

  protected boolean nodeExists(String path) {
    final long start = System.currentTimeMillis();

    try {
      Stat stat = curatorFramework.checkExists().forPath(path);
      log(OperationType.READ, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      return stat != null;
    } catch (KeeperException.NoNodeException e) {
      return false;
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected <T> void writeToZk(String path, T data) {
    final long start = System.currentTimeMillis();

    try {
      final byte[] serializedInfo = serialize(data);

      final PathAndBytesable<?> builder;

      if (curatorFramework.checkExists().forPath(path) != null) {
        builder = curatorFramework.setData();
      } else {
        builder = curatorFramework.create().creatingParentsIfNeeded();
      }

      builder.forPath(path, serializedInfo);
      log(OperationType.WRITE, Optional.<Integer>absent(), Optional.of(serializedInfo.length), start, path);
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected <T> byte[] serialize(T data) {
    try {
      return objectMapper.writeValueAsBytes(data);
    } catch (JsonProcessingException e) {
      throw Throwables.propagate(e);
    }
  }

  protected <T> Optional<T> readFromZk(final String path, final Class<T> klass) {
    final long start = System.currentTimeMillis();

    Optional<byte[]> data = readFromZk(path);

    if (data.isPresent()) {
      log(OperationType.READ, Optional.<Integer>absent(), Optional.of(data.get().length), start, path);
      if (data.get().length > 0) {
        return Optional.of(deserialize(data.get(), klass, path));
      }
    }
    return Optional.absent();
  }

  protected Optional<byte[]> readFromZk(String path) {
    try {
      byte[] data = curatorFramework.getData().forPath(path);
      if (data.length > 0) {
        return Optional.of(curatorFramework.getData().forPath(path));
      } else {
        return Optional.absent();
      }
    } catch (KeeperException.NoNodeException nne) {
      return Optional.absent();
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected <T> T deserialize(byte[] data, Class<T> klass, String path) {
    try {
      return objectMapper.readValue(data, klass);
    } catch (JsonParseException jpe) {
      try {
        LOG.error("Invalid Json at path {}: {}", path, new String(data, StandardCharsets.UTF_8), jpe);
      } catch (Exception e) {
        LOG.error("Could not get raw json string at path {}", path, e);
      }
      throw Throwables.propagate(jpe);
    } catch (IOException e) {
      throw Throwables.propagate(e);
    }
  }

  protected String createNode(String path) {
    final long start = System.currentTimeMillis();

    try {
      final String result = curatorFramework.create().creatingParentsIfNeeded().forPath(path);
      log(OperationType.WRITE, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      return result;
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected String createPersistentSequentialNode(String path) {
    final long start = System.currentTimeMillis();

    try {
      final String result = curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath(path);
      log(OperationType.WRITE, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      return result;
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected boolean deleteNode(String path) {
    return deleteNode(path, false);
  }

  protected boolean deleteNode(String path, boolean recursive) {
    final long start = System.currentTimeMillis();

    try {
      if (recursive) {
        curatorFramework.delete().deletingChildrenIfNeeded().forPath(path);
        log(OperationType.WRITE, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      } else {
        curatorFramework.delete().forPath(path);
        log(OperationType.WRITE, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      }
      return true;
    } catch (KeeperException.NoNodeException e) {
      return false;
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected List<String> getChildren(String path) {
    final long start = System.currentTimeMillis();

    try {
      List<String> children = curatorFramework.getChildren().forPath(path);
      log(OperationType.READ, Optional.of(children.size()), Optional.<Integer>absent(), start, path);
      return children;
    } catch (KeeperException.NoNodeException e) {
      return Collections.emptyList();
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  protected Optional<Long> getUpdatedAt(String path) {
    final long start = System.currentTimeMillis();

    try {
      Stat stat = curatorFramework.checkExists().forPath(path);
      log(OperationType.READ, Optional.<Integer>absent(), Optional.<Integer>absent(), start, path);
      if (stat != null) {
        return Optional.of(stat.getMtime());
      } else {
        return Optional.absent();
      }
    } catch (KeeperException.NoNodeException e) {
      return Optional.absent();
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }
}