/*
 * 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.ratis.server.impl;

import org.apache.ratis.proto.RaftProtos;
import org.apache.ratis.protocol.RaftPeer;
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.server.DivisionInfo;
import org.apache.ratis.server.impl.LeaderElection.Phase;
import org.apache.ratis.server.protocol.TermIndex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class VoteContext {
  static final Logger LOG = LoggerFactory.getLogger(VoteContext.class);

  private final RaftServerImpl impl;
  private final RaftConfigurationImpl conf;
  private final Phase phase;
  private final RaftPeerId candidateId;

  VoteContext(RaftServerImpl impl, Phase phase, RaftPeerId candidateId) {
    this.impl = impl;
    this.conf = impl.getRaftConf();
    this.phase = phase;
    this.candidateId = candidateId;
  }

  private boolean reject(String reason) {
    return log(false, reason);
  }

  private boolean log(boolean accept, String reason) {
    LOG.info("{}-{}: {} {} from {}: {}",
        impl.getMemberId(), impl.getInfo().getCurrentRole(), accept? "accept": "reject", phase, candidateId, reason);
    return accept;
  }

  /** Check if the candidate is in the current conf. */
  private RaftPeer checkConf() {
    if (!conf.containsInConf(candidateId)) {
      reject(candidateId + " is not in current conf " + conf.getCurrentPeers());
      return null;
    }
    return conf.getPeer(candidateId);
  }

  enum CheckTermResult {
    FAILED, CHECK_LEADER, SKIP_CHECK_LEADER
  }

  /** Check the candidate term. */
  private CheckTermResult checkTerm(long candidateTerm) {
    if (phase == Phase.PRE_VOTE) {
      return CheckTermResult.CHECK_LEADER;
    }
    // check term
    final ServerState state = impl.getState();
    final long currentTerm = state.getCurrentTerm();
    if (currentTerm > candidateTerm) {
      reject("current term " + currentTerm + " > candidate's term " + candidateTerm);
      return CheckTermResult.FAILED;
    } else if (currentTerm == candidateTerm) {
      // check if this server has already voted
      final RaftPeerId votedFor = state.getVotedFor();
      if (votedFor != null && !votedFor.equals(candidateId)) {
        reject("already has voted for " + votedFor + " at current term " + currentTerm);
        return CheckTermResult.FAILED;
      }
      return CheckTermResult.CHECK_LEADER;
    } else {
      return CheckTermResult.SKIP_CHECK_LEADER; //currentTerm < candidateTerm
    }
  }

  /** Check if there is already a leader. */
  private boolean checkLeader() {
    // check if this server is the leader
    final DivisionInfo info = impl.getInfo();
    if (info.isLeader()) {
      if (impl.getRole().getLeaderState().map(LeaderStateImpl::checkLeadership).orElse(false)) {
        return reject("this server is the leader and still has leadership");
      }
    }

    // check if this server is a follower and has a valid leader
    if (info.isFollower()) {
      final RaftPeerId leader = impl.getState().getLeaderId();
      if (leader != null
          && !leader.equals(candidateId)
          && impl.getRole().getFollowerState().map(FollowerState::isCurrentLeaderValid).orElse(false)) {
        return reject("this server is a follower and still has a valid leader " + leader
            + " different than the candidate " + candidateId);
      }
    }
    return true;
  }

  RaftPeer recognizeCandidate(long candidateTerm) {
    final RaftPeer candidate = checkConf();
    if (candidate == null) {
      return null;
    }
    final CheckTermResult r = checkTerm(candidateTerm);
    if (r == CheckTermResult.FAILED) {
      return null;
    }
    if (r == CheckTermResult.CHECK_LEADER && !checkLeader()) {
      return null;
    }
    return candidate;
  }

  /**
   * A server should vote for candidate if:
   * 1. log lags behind candidate, or
   * 2. log equals candidate's, and priority less or equal candidate's
   *
   * See Section 5.4.1 Election restriction
   */
  boolean decideVote(RaftPeer candidate, TermIndex candidateLastEntry) {
    if (impl.getRole().getCurrentRole() == RaftProtos.RaftPeerRole.LISTENER) {
      return reject("this server is a listener, who is a non-voting member");
    }
    if (candidate == null) {
      return false;
    }
    // Check last log entry
    final TermIndex lastEntry = impl.getState().getLastEntry();
    final int compare = ServerState.compareLog(lastEntry, candidateLastEntry);
    if (compare < 0) {
      return log(true, "our last entry " + lastEntry + " < candidate's last entry " + candidateLastEntry);
    } else if (compare > 0) {
      return reject("our last entry " + lastEntry + " > candidate's last entry " + candidateLastEntry);
    }

    // Check priority
    final RaftPeer peer = conf.getPeer(impl.getId());
    if (peer == null) {
      return reject("our server " + impl.getId() + " is not in the conf " + conf);
    }
    final int priority = peer.getPriority();
    if (priority <= candidate.getPriority()) {
      return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
    } else {
      return reject("our priority " + priority + " > candidate's priority " + candidate.getPriority());
    }
  }
}
