/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.client.solrj.cloud.autoscaling;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.util.Pair;

import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.MOVEREPLICA;

/**
 * Implements the 'withCollection' variable type
 */
public class WithCollectionVariable extends VariableBase {

  public WithCollectionVariable(Type type) {
    super(type);
  }

  @Override
  public boolean match(Object inputVal, Operand op, Object val, String name, Row row) {
    @SuppressWarnings({"unchecked"})
    Map<String, String> withCollectionMap = (Map<String, String>) inputVal;
    if (withCollectionMap == null || withCollectionMap.isEmpty()) return true;

    Set<String> uniqueColls = new HashSet<>();
    row.forEachReplica(replicaInfo -> uniqueColls.add(replicaInfo.getCollection()));

    for (Map.Entry<String, String> e : withCollectionMap.entrySet()) {
      if (uniqueColls.contains(e.getKey()) && !uniqueColls.contains(e.getValue())) return false;
    }

    return true;
  }

  public void projectAddReplica(Cell cell, ReplicaInfo ri, Consumer<Row.OperationInfo> opCollector, boolean strictMode) {
    if (strictMode) {
      // we do not want to add a replica of the 'withCollection' in strict mode
      return;
    }

    @SuppressWarnings({"unchecked"})
    Map<String, String> withCollectionMap = (Map<String, String>) cell.val;
    if (withCollectionMap == null || withCollectionMap.isEmpty()) return;

    Set<String> uniqueColls = new HashSet<>();
    Row row = cell.row;
    row.forEachReplica(replicaInfo -> uniqueColls.add(replicaInfo.getCollection()));

    for (Map.Entry<String, String> e : withCollectionMap.entrySet()) {
      if (uniqueColls.contains(e.getKey()) && !uniqueColls.contains(e.getValue())) {
        String withCollection = e.getValue();

        opCollector.accept(new Row.OperationInfo(withCollection, "shard1", row.node, cell.name, true, Replica.Type.NRT));
      }
    }
  }

  @Override
  public int compareViolation(Violation v1, Violation v2) {
    return Integer.compare(v1.getViolatingReplicas().size(), v2.getViolatingReplicas().size());
  }

  public boolean addViolatingReplicas(Violation.Ctx ctx) {
    String node = ctx.currentViolation.node;
    for (Row row : ctx.allRows) {
      if (node.equals(row.node)) {
        @SuppressWarnings({"unchecked"})
        Map<String, String> withCollectionMap = (Map<String, String>) row.getVal("withCollection");
        if (withCollectionMap != null) {
          row.forEachReplica(r -> {
            String withCollection = withCollectionMap.get(r.getCollection());
            if (withCollection != null) {
              // test whether this row has at least 1 replica of withCollection, else there is a violation
              Set<String> uniqueCollections = new HashSet<>();
              row.forEachReplica(replicaInfo -> uniqueCollections.add(replicaInfo.getCollection()));
              if (!uniqueCollections.contains(withCollection)) {
                ctx.currentViolation.addReplica(new Violation.ReplicaInfoAndErr(r).withDelta(1.0d));
              }
            }
          });
          ctx.currentViolation.replicaCountDelta = (double) ctx.currentViolation.getViolatingReplicas().size();
        }
      }
    }
    return true;
  }

  @Override
  public void getSuggestions(Suggestion.Ctx ctx) {
    if (ctx.violation.getViolatingReplicas().isEmpty()) return;

    Map<String, Object> nodeValues = ctx.session.nodeStateProvider.getNodeValues(ctx.violation.node, Collections.singleton("withCollection"));
    @SuppressWarnings({"unchecked"})
    Map<String, String> withCollectionsMap = (Map<String, String>) nodeValues.get("withCollection");
    if (withCollectionsMap == null) return;

    Set<String> uniqueCollections = new HashSet<>();
    for (Violation.ReplicaInfoAndErr replicaInfoAndErr : ctx.violation.getViolatingReplicas()) {
      uniqueCollections.add(replicaInfoAndErr.replicaInfo.getCollection());
    }

    collectionLoop:
    for (String collection : uniqueCollections) {
      String withCollection = withCollectionsMap.get(collection);
      if (withCollection == null) continue;

      // can we find a node from which we can move a replica of the `withCollection`
      // without creating another violation?
      for (Row row : ctx.session.matrix) {
        if (ctx.violation.node.equals(row.node))  continue; // filter the violating node

        Set<String> hostedCollections = new HashSet<>();
        row.forEachReplica(replicaInfo -> hostedCollections.add(replicaInfo.getCollection()));

        if (hostedCollections.contains(withCollection) && !hostedCollections.contains(collection))  {
          // find the candidate replicas that we can move
          List<ReplicaInfo> movableReplicas = new ArrayList<>();
          row.forEachReplica(replicaInfo -> {
            if (replicaInfo.getCollection().equals(withCollection)) {
              movableReplicas.add(replicaInfo);
            }
          });

          for (ReplicaInfo toMove : movableReplicas) {
            // candidate source node for a move replica operation
            Suggester suggester = ctx.session.getSuggester(MOVEREPLICA)
                .forceOperation(true)
                .hint(Suggester.Hint.COLL_SHARD, new Pair<>(withCollection, "shard1"))
                .hint(Suggester.Hint.SRC_NODE, row.node)
                .hint(Suggester.Hint.REPLICA, toMove.getName())
                .hint(Suggester.Hint.TARGET_NODE, ctx.violation.node);
            if (ctx.addSuggestion(suggester) != null)
              continue collectionLoop; // one suggestion is enough for this collection
          }
        }
      }

      // we could not find a valid move, so we suggest adding a replica
      Suggester suggester = ctx.session.getSuggester(ADDREPLICA)
          .forceOperation(true)
          .hint(Suggester.Hint.COLL_SHARD, new Pair<>(withCollection, "shard1"))
          .hint(Suggester.Hint.TARGET_NODE, ctx.violation.node);
      ctx.addSuggestion(suggester);
    }
  }
}
