package com.airlock.iam.plugin.configeditor.ui.flowviz;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public final class FlowVisualizationModelSerializer {
   private final AtomicInteger idGenerator = new AtomicInteger();
   private final FlowVisualizationModel model;
   private final Map<FlowVisualizationModel.Step, FlowVisualizationModelSerializer.NodeId> step2Id = new IdentityHashMap<>();

   public FlowVisualizationModelSerializer(FlowVisualizationModel model) {
      this.model = model;
   }

   public String convertToJson() {
      ObjectMapper mapper = new ObjectMapper();
      ObjectNode root = mapper.createObjectNode();
      root.put("title", this.model.title());
      root.put("environment", this.environmentIdentifier());
      root.put("context", this.contextIdentifier());
      root.put("class", "go.GraphLinksModel");
      ArrayNode nodeDataArray = root.putArray("nodeData");
      ArrayNode linkDataArray = root.putArray("linkData");
      List<FlowVisualizationModel.Step> steps = this.model.flow().steps();
      FlowVisualizationModelSerializer.NodeId startNodeId = new FlowVisualizationModelSerializer.NodeId(0);
      ObjectNode startNode = createNode(startNodeId, "Start", "start", mapper);
      nodeDataArray.add(startNode);
      FlowVisualizationModelSerializer.NodesToConnect nodesToConnect;
      if (steps.isEmpty()) {
         nodesToConnect = new FlowVisualizationModelSerializer.NodesToConnect(startNodeId, List.of(startNodeId));
      } else {
         nodesToConnect = this.createStepNodes(steps, mapper, nodeDataArray, linkDataArray);
         linkDataArray.add(createSimpleLink(startNodeId, nodesToConnect.in(), mapper));
      }

      FlowVisualizationModelSerializer.NodeId endNodeId = this.createId();
      ObjectNode endNode = createNode(endNodeId, "End", "end", mapper);
      nodeDataArray.add(endNode);
      nodesToConnect.out().stream().map(id -> createSimpleLink(id, endNodeId, mapper)).forEach(linkDataArray::add);

      try {
         return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root);
      } catch (JsonProcessingException e) {
         throw new FailureException(e);
      }
   }

   private String environmentIdentifier() {
      return this.model.environment().getIdentifier().orElse("common");
   }

   private String contextIdentifier() {
      return this.model.context().isDefaultContext() ? "default" : this.model.context().getIdentifier();
   }

   private void extractStepNodeIds(List<FlowVisualizationModel.Step> steps) {
      for (FlowVisualizationModel.Step step : steps) {
         if (!this.step2Id.containsKey(step)) {
            this.step2Id.put(step, this.createId());
         }

         if (step instanceof FlowVisualizationModel.SelectionStep selectionStep) {
            for (FlowVisualizationModel.SelectionOption option : selectionStep.getOptions()) {
               this.extractStepNodeIds(option.getFlow().steps());
            }

            if (selectionStep.getFallback().isPresent()) {
               this.extractStepNodeIds(selectionStep.getFallback().orElseThrow().steps());
            }
         }
      }
   }

   private FlowVisualizationModelSerializer.NodesToConnect createStepNodes(
      List<FlowVisualizationModel.Step> steps, ObjectMapper mapper, ArrayNode nodeDataArray, ArrayNode linkDataArray
   ) {
      this.extractStepNodeIds(steps);
      FlowVisualizationModelSerializer.NodesToConnect nodesToConnect = null;
      FlowVisualizationModelSerializer.NodesToConnect first = null;
      FlowVisualizationModelSerializer.NodesToConnect prev = null;

      for (FlowVisualizationModel.Step step : steps) {
         if (step instanceof FlowVisualizationModel.SelectionStep) {
            nodesToConnect = this.createSelectionNode((FlowVisualizationModel.SelectionStep)step, mapper, nodeDataArray, linkDataArray);
         } else {
            nodesToConnect = this.createStepNode(step, mapper, nodeDataArray);
         }

         if (first == null) {
            first = nodesToConnect;
         }

         if (prev != null) {
            FlowVisualizationModelSerializer.NodeId inNode = nodesToConnect.in();
            prev.out().stream().map(id -> createSimpleLink(id, inNode, mapper)).forEach(linkDataArray::add);
         }

         prev = nodesToConnect;
      }

      if (first != null && nodesToConnect.out() != null) {
         for (FlowVisualizationModel.Step step : steps) {
            step.getGotoTargets().stream().map(target -> createGotoLink(this.step2Id.get(step), this.nodeId(target), mapper)).forEach(linkDataArray::add);
            step.getDynamicStepActivations()
               .stream()
               .map(activation -> createDsaLink(this.step2Id.get(step), this.nodeId(activation), mapper))
               .forEach(linkDataArray::add);
         }

         return new FlowVisualizationModelSerializer.NodesToConnect(first.in(), nodesToConnect.out());
      } else {
         throw new FailureException("Failed to create step nodes.");
      }
   }

   private FlowVisualizationModelSerializer.NodesToConnect createStepNode(FlowVisualizationModel.Step step, ObjectMapper mapper, ArrayNode nodeDataArray) {
      FlowVisualizationModelSerializer.NodeId id = this.step2Id.get(step);
      ObjectNode node = createNode(id, step.getStepName(), step instanceof FlowVisualizationModel.SelectionStep ? "selection" : "step", mapper);
      step.getStepId().ifPresent(stepId -> node.put("stepId", stepId));
      step.getPreCondition().ifPresent(condition -> node.put("preCondition", condition));
      step.getSkipCondition().ifPresent(condition -> node.put("skipCondition", condition));
      if (!step.getTagsOnSuccess().isEmpty()) {
         ArrayNode arrayNode = node.putArray("tagsOnSuccess");
         step.getTagsOnSuccess().forEach(arrayNode::add);
      }

      node.put("interactive", step.isInteractive());
      if (step.isRequiresActivation()) {
         node.put("requiresActivation", true);
      }

      nodeDataArray.add(node);
      return new FlowVisualizationModelSerializer.NodesToConnect(id, List.of(id));
   }

   private FlowVisualizationModelSerializer.NodesToConnect createSelectionNode(
      FlowVisualizationModel.SelectionStep selectionStep, ObjectMapper mapper, ArrayNode nodeDataArray, ArrayNode linkDataArray
   ) {
      FlowVisualizationModelSerializer.NodesToConnect selectionNode = this.createStepNode(selectionStep, mapper, nodeDataArray);
      List<FlowVisualizationModelSerializer.NodeId> outIds = new ArrayList<>();

      for (FlowVisualizationModel.SelectionOption option : selectionStep.getOptions()) {
         this.createSelectionOptionFlow(option, selectionNode, mapper, outIds, nodeDataArray, linkDataArray);
      }

      if (selectionStep.getFallback().isPresent()) {
         this.createSelectionFallbackFlow(selectionStep, selectionNode, mapper, outIds, nodeDataArray, linkDataArray);
      }

      return new FlowVisualizationModelSerializer.NodesToConnect(selectionNode.in, outIds);
   }

   private void createSelectionOptionFlow(
      FlowVisualizationModel.SelectionOption option,
      FlowVisualizationModelSerializer.NodesToConnect selectionNode,
      ObjectMapper mapper,
      List<FlowVisualizationModelSerializer.NodeId> outIds,
      ArrayNode nodeDataArray,
      ArrayNode linkDataArray
   ) {
      FlowVisualizationModelSerializer.NodeId optionId = this.createId();
      ObjectNode optionNode = createNode(optionId, option.getName(), "selectionOption", mapper);
      option.getCondition().ifPresent(condition -> optionNode.put("condition", condition));
      nodeDataArray.add(optionNode);
      linkDataArray.add(createSimpleLink(selectionNode.in, optionId, mapper));
      this.createSubflow(optionId, option.getFlow(), mapper, nodeDataArray, linkDataArray, outIds);
   }

   private void createSelectionFallbackFlow(
      FlowVisualizationModel.SelectionStep selectionStep,
      FlowVisualizationModelSerializer.NodesToConnect selectionNode,
      ObjectMapper mapper,
      List<FlowVisualizationModelSerializer.NodeId> outIds,
      ArrayNode nodeDataArray,
      ArrayNode linkDataArray
   ) {
      FlowVisualizationModelSerializer.NodeId fallbackId = this.createId();
      ObjectNode node = createNode(fallbackId, "Fallback", "selectionOption", mapper);
      nodeDataArray.add(node);
      linkDataArray.add(createSimpleLink(selectionNode.in, fallbackId, mapper));
      this.createSubflow(fallbackId, selectionStep.getFallback().orElseThrow(), mapper, nodeDataArray, linkDataArray, outIds);
   }

   private void createSubflow(
      FlowVisualizationModelSerializer.NodeId optionId,
      FlowVisualizationModel.Flow flow,
      ObjectMapper mapper,
      ArrayNode nodeDataArray,
      ArrayNode linkDataArray,
      List<FlowVisualizationModelSerializer.NodeId> outIds
   ) {
      FlowVisualizationModelSerializer.NodesToConnect nodesToConnect = this.createStepNodes(flow.steps(), mapper, nodeDataArray, linkDataArray);
      outIds.addAll(nodesToConnect.out());
      linkDataArray.add(createSimpleLink(optionId, nodesToConnect.in(), mapper));
   }

   private static ObjectNode createSimpleLink(FlowVisualizationModelSerializer.NodeId from, FlowVisualizationModelSerializer.NodeId to, ObjectMapper mapper) {
      return createLink(from, to, mapper, Map.of());
   }

   private static ObjectNode createGotoLink(FlowVisualizationModelSerializer.NodeId from, FlowVisualizationModelSerializer.NodeId to, ObjectMapper mapper) {
      return createLink(from, to, mapper, Map.of("category", "goto"));
   }

   private static ObjectNode createDsaLink(FlowVisualizationModelSerializer.NodeId from, FlowVisualizationModelSerializer.NodeId to, ObjectMapper mapper) {
      return createLink(from, to, mapper, Map.of("category", "dsa"));
   }

   private static ObjectNode createLink(
      FlowVisualizationModelSerializer.NodeId from, FlowVisualizationModelSerializer.NodeId to, ObjectMapper mapper, Map<String, String> meta
   ) {
      ObjectNode link = mapper.createObjectNode();
      link.put("from", from.value());
      link.put("to", to.value());
      link.setAll(meta.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> new TextNode((String)e.getValue()))));
      return link;
   }

   private static ObjectNode createNode(FlowVisualizationModelSerializer.NodeId key, String title, String category, ObjectMapper mapper) {
      return createNode(key, title, category, mapper, Map.of());
   }

   private static ObjectNode createNode(
      FlowVisualizationModelSerializer.NodeId key, String title, String category, ObjectMapper mapper, Map<String, String> meta
   ) {
      ObjectNode node = mapper.createObjectNode();
      node.put("key", key.value());
      node.put("title", title);
      node.put("category", category);
      node.setAll(meta.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> new TextNode((String)e.getValue()))));
      return node;
   }

   private FlowVisualizationModelSerializer.NodeId nodeId(String stepId) {
      return this.step2Id
         .entrySet()
         .stream()
         .filter(e -> e.getKey().getStepId().map(id -> id.equals(stepId)).orElse(false))
         .map(Entry::getValue)
         .findFirst()
         .orElseThrow(() -> new FailureException("Unknown Step ID: " + stepId + " - available Step IDs: " + this.availableStepIdsAsString()));
   }

   private String availableStepIdsAsString() {
      return this.step2Id
         .keySet()
         .stream()
         .map(FlowVisualizationModel.Step::getStepId)
         .filter(Optional::isPresent)
         .map(Optional::get)
         .collect(Collectors.joining(", "));
   }

   private FlowVisualizationModelSerializer.NodeId createId() {
      return new FlowVisualizationModelSerializer.NodeId(this.idGenerator.incrementAndGet());
   }

   private record NodeId(int value) {
   }

   private record NodesToConnect(FlowVisualizationModelSerializer.NodeId in, List<FlowVisualizationModelSerializer.NodeId> out) {
   }
}
