package org.infinispan.server.cli.util;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.infinispan.server.cli.CliInterpreterException;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandFormatException;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.cli.Util;
import org.jboss.as.cli.operation.OperationFormatException;
import org.jboss.as.cli.operation.OperationRequestAddress;
import org.jboss.as.cli.operation.impl.DefaultCallbackHandler;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;


/**
 * Some utility methods.
 *
 * @author Pedro Ruivo
 * @since 6.1
 */
public class InfinispanUtil {

   private static final String SUBSYSTEM = "subsystem";
   private static final String INFINISPAN = "datagrid-infinispan";
   private static final String INFINISPAN_SUBSYSTEM = "/" + SUBSYSTEM + "=" + INFINISPAN;
   private static final String CONTAINER_TYPE = "cache-container";
   private static final String CONTAINER_ADDRESS = INFINISPAN_SUBSYSTEM + "/" + CONTAINER_TYPE;

   private InfinispanUtil() {
   }

   public static List<String> getContainerNames(CommandContext ctx) throws OperationFormatException {
      OperationRequestAddress address = buildOperationRequest(ctx, INFINISPAN_SUBSYSTEM);
      return Util.getNodeNames(ctx.getModelControllerClient(), address, CONTAINER_TYPE);
   }

   public static boolean containsContainer(CommandContext ctx, String container) throws OperationFormatException {
      return new HashSet<String>(getContainerNames(ctx)).contains(container);
   }

   public static void changeToContainer(CommandContext ctx, String container) throws CommandLineException {
      if (!containsContainer(ctx, container)) {
         throw new CommandLineException("No such container: " + container);
      }
      OperationRequestAddress address = ctx.getCurrentNodePath();
      address.reset();
      address.toNode("subsystem", INFINISPAN);
      address.toNode(CONTAINER_TYPE, container);
   }

   public static Map<String, List<String>> getCachesNames(CommandContext ctx, String container) throws CommandLineException {
      if (!containsContainer(ctx, container)) {
         throw new CommandLineException("No such container: " + container);
      }
      OperationRequestAddress address = buildOperationRequest(ctx, CONTAINER_ADDRESS + "=" + container);
      List<String> cacheModes = Util.getNodeTypes(ctx.getModelControllerClient(), address);
      Map<String, List<String>> caches = new HashMap<String, List<String>>();
      for (String cacheMode : cacheModes) {
         caches.put(cacheMode, Util.getNodeNames(ctx.getModelControllerClient(), address, cacheMode));
      }
      return caches;
   }

   public static void changeToCache(CommandContext ctx, String container, String cache) throws CommandLineException {
      Map<String, List<String>> caches = getCachesNames(ctx, container);
      for (Map.Entry<String, List<String>> entry : caches.entrySet()) {
         if (entry.getValue().contains(cache)) {
            changeToContainer(ctx, container);
            OperationRequestAddress address = ctx.getCurrentNodePath();
            address.toNode(entry.getKey(), cache);
            return;
         }
      }
   }

   public static void connect(CommandContext ctx, String container, String cache) throws CommandLineException {
      if (container == null) {
         List<String> containers = getContainerNames(ctx);
         if (containers.isEmpty()) {
            throw new CommandLineException("No containers found!");
         }
         container = containers.get(0);
      }
      try {
         if (cache != null) {
            changeToCache(ctx, container, cache);
            cliRequest(ctx, "cache " + cache + "\n");
         } else {
            changeToContainer(ctx, container);
         }
      } catch (CommandLineException e) {
         ctx.getCurrentNodePath().reset();
         throw e;
      } catch (CliInterpreterException e) {
         ctx.getCurrentNodePath().reset();
         throw new CommandLineException(e.getLocalizedMessage());
      }
   }

   public static void disconnect(CommandContext ctx) {
      ctx.getCurrentNodePath().reset();
   }

   public static ModelNode cliRequest(CommandContext ctx, String command) throws CommandLineException, CliInterpreterException {
      final ModelNode request = buildCliRequest(ctx, command);

      final ModelControllerClient client = ctx.getModelControllerClient();
      final ModelNode response;
      try {
         response = client.execute(request);
      } catch (Exception e) {
         throw new CommandFormatException("Failed to perform operation: " + e.getLocalizedMessage());
      }
      if (!Util.isSuccess(response)) {
         throw new CommandFormatException(Util.getFailureDescription(response));
      }
      if (!response.has(Util.RESULT)) {
         return null;
      }
      ModelNode result = response.get(Util.RESULT);
      if (Boolean.parseBoolean(result.get("isError").asString())) {
         throw new CliInterpreterException(result.get("result").asString());
      }
      updateStateFromResponse(result, ctx);
      return result;
   }

   public static ModelNode buildCliRequest(CommandContext ctx, String command) throws CommandLineException {
      final CacheInfo cacheInfo = getCacheInfo(ctx);
      OperationRequestAddress requestAddress = getContainerAddress(ctx, cacheInfo.getContainer());
      ModelNode req = Util.buildRequest(ctx, requestAddress, "cli-interpreter");
      updateRequest(req, ctx, command, cacheInfo);
      return req;
   }

   public static void updateStateFromResponse(ModelNode node, CommandContext context) {
      if (node.has("sessionId")) {
         context.set(getCacheInfo(context).getContainer() + "-sessionId", node.get("sessionId").asString());
      }
   }

   public static OperationRequestAddress buildOperationRequest(CommandContext ctx, String address) throws OperationFormatException {
      DefaultCallbackHandler handler = new DefaultCallbackHandler();
      ctx.getCommandLineParser().parse(address, handler);
      return handler.getAddress();
   }

   /**
    * @return the container and cache name.
    */
   public static CacheInfo getCacheInfo(CommandContext context) {
      String container = null;
      String cache = null;
      final Iterator<OperationRequestAddress.Node> iterator = context.getCurrentNodePath().iterator();
      for (int i = 0; i < 3 && iterator.hasNext(); ++i) {
         OperationRequestAddress.Node node = iterator.next();
         if (node.getType().equals(CONTAINER_TYPE)) {
            container = node.getName();
         } else if (node.getType().endsWith("-cache")) {
            cache = node.getName();
         }
      }
      return new CacheInfo(container, cache);
   }

   private static void updateRequest(ModelNode request, CommandContext context, String command, CacheInfo cacheInfo) {
      setInModelNode(request, "cacheName", cacheInfo.getCache());
      setInModelNode(request, "sessionId", (String) context.get(cacheInfo.getContainer() + "-sessionId"));
      setInModelNode(request, "command", command);
   }

   private static void setInModelNode(ModelNode node, String key, String value) {
      if (value != null) {
         node.get(key).set(value);
      }
   }

   private static OperationRequestAddress getContainerAddress(CommandContext ctx, String container) throws CommandLineException {
      if (container == null) {
         throw new CommandLineException("Container does not exists");
      }
      return buildOperationRequest(ctx, CONTAINER_ADDRESS + "=" + container);
   }

   public static class CacheInfo {
      private final String container;
      private final String cache;

      public CacheInfo(String container, String cache) {
         this.container = container;
         this.cache = cache;
      }

      public String getContainer() {
         return container;
      }

      public String getCache() {
         return cache;
      }
   }

}
