#include <common/net/message/nodes/SetMirrorBuddyGroupMsg.h>
#include <common/net/message/nodes/SetMirrorBuddyGroupRespMsg.h>
#include <common/storage/StorageTargetInfo.h>
#include <common/toolkit/MessagingTk.h>
#include <common/toolkit/NodesTk.h>
#include <common/toolkit/ZipIterator.h>

#include "MirrorBuddyGroupCreator.h"

#include <boost/lexical_cast.hpp>

/**
 * @return FhgfsOpsErr_...
 */
FhgfsOpsErr MirrorBuddyGroupCreator::addGroup(uint16_t primaryTargetID, uint16_t secondaryTargetID,
   uint16_t forcedGroupID)
{
   LogContext log = LogContext("MirrorBuddyGroupCreator (addGroup)");

   FhgfsOpsErr retVal = FhgfsOpsErr_INTERNAL;

   // disallow meta buddy group creation if the selected secondary target would have the root inode,
   // not the primary. while we could copy the root inode from secondary to primary, forcing the
   // non-mirrored root inode owner to be the primary of a group is much less error-prone.
   if (nodeType == NODETYPE_Meta && secondaryTargetID == metaRoot->getID().val())
      return FhgfsOpsErr_NOTOWNER;

   uint16_t newGroupID;
   const FhgfsOpsErr addRes = addGroupComm(primaryTargetID, secondaryTargetID, forcedGroupID,
                                           newGroupID);

   if (addRes != FhgfsOpsErr_SUCCESS)
   {
      std::string errStr;

      if (addRes == FhgfsOpsErr_INUSE)
         errStr = "One of the targets is already mapped to a buddy group.";
      else
      if (addRes == FhgfsOpsErr_UNKNOWNTARGET || addRes == FhgfsOpsErr_UNKNOWNNODE)
         errStr = boost::lexical_cast<std::string>(addRes);
      else
      if (addRes == FhgfsOpsErr_EXISTS)
         errStr = "Mirror buddy group with ID " + StringTk::uintToStr(forcedGroupID)
            + " already exists.";
      else
         errStr = "Internal error (Please check metadata server log).";

      const std::string logMessage("Failed to add mirror buddy group: primaryTargetID " +
         StringTk::uintToStr(primaryTargetID) + "; secondaryTargetID " +
         StringTk::uintToStr(secondaryTargetID) + "; Error: " + errStr);
      std::cerr << logMessage << std::endl;
      log.logErr(logMessage);
   }
   else
   {
      const std::string logMessage("Mirror buddy group successfully set: groupID " +
         StringTk::uintToStr(newGroupID) + " -> target IDs " +
         StringTk::uintToStr(primaryTargetID) + ", " + StringTk::uintToStr(secondaryTargetID) );
      std::cout << logMessage << std::endl;
      log.log(Log_NOTICE, logMessage);

      retVal = FhgfsOpsErr_SUCCESS;
   }

   return retVal;
}

/*
 * @param forcedGroupID 0 means create new group, otherwise create group with that ID
 * @param outNewGroupID will be set by response message; will have the generated ID of a newly
 *    created group or the old group ID if a group was changed
 * @return FhgfsOpsErr_...
 */
FhgfsOpsErr MirrorBuddyGroupCreator::addGroupComm(uint16_t primaryID, uint16_t secondaryID,
   uint16_t forcedGroupID, uint16_t& outNewGroupID) const
{
   FhgfsOpsErr retVal = FhgfsOpsErr_INTERNAL;

   const auto mgmtNode = this->mgmtNodes->referenceFirstNode();

   SetMirrorBuddyGroupRespMsg* respMsgCast;

   SetMirrorBuddyGroupMsg msg(nodeType, primaryID, secondaryID, forcedGroupID, false);

   const auto respMsg = MessagingTk::requestResponse(*mgmtNode, msg,
         NETMSGTYPE_SetMirrorBuddyGroupResp);
   if(!respMsg)
   {
      retVal = FhgfsOpsErr_COMMUNICATION;
      goto err_cleanup;
   }

   respMsgCast = (SetMirrorBuddyGroupRespMsg*)respMsg.get();

   retVal = (FhgfsOpsErr)respMsgCast->getResult();

   if (forcedGroupID != 0)
      outNewGroupID = forcedGroupID;
   else
      outNewGroupID = respMsgCast->getBuddyGroupID();

err_cleanup:
   return retVal;
}

/**
 * Create all MirrorBuddyGroups which was generated by the automatic mode
 *
 * @param retValGeneration the return value of the MirrorBuddyGroup generation
 * @param buddyGroupIDs A list with all MirrorBuddyGroupIDs to create
 * @param buddyGroups A list with all MirrorBuddyGroups to create
 * @return true if all MirrorBuddyGroups created successful, false if not
 */
bool MirrorBuddyGroupCreator::createMirrorBuddyGroups(FhgfsOpsErr retValGeneration,
   const UInt16List* buddyGroupIDs, const MirrorBuddyGroupList* buddyGroups)
{
   if( (retValGeneration == FhgfsOpsErr_SUCCESS) ||
      ( (retValGeneration == FhgfsOpsErr_INVAL) && this->cfgForce) )
   { // create the MirrorBuddyGroup
      if(!this->cfgDryrun)
      { // but only when it is not a dryrun
         for(ZipConstIterRange<UInt16List, MirrorBuddyGroupList> iter(*buddyGroupIDs, *buddyGroups);
              !iter.empty(); ++iter)
         {
            FhgfsOpsErr retValAddGroup = addGroup(
                  (iter()->second)->firstTargetID,
                  (iter()->second)->secondTargetID,
                  *(iter()->first));

            if(retValAddGroup != FhgfsOpsErr_SUCCESS)
               return false;
         }
      }
   }

   return true;
}

/**
 * Removes all nodes / targets from the node ID list which are part of a mirror buddy group.
 *
 * @param oldPrimaryIDs PrimaryTargetIDs from existing MirrorBuddyGroups
 * @param oldSecondaryIDs SecondaryTargetIDs from existing MirrorBuddyGroups
 * @return true if all targets successful removed from TargetMapper, false on error
 */
bool MirrorBuddyGroupCreator::removeTargetsFromExistingMirrorBuddyGroups(
      const UInt16List* oldPrimaryIDs, const UInt16List* oldSecondaryIDs)
{
   bool retVal = true;

   for (ZipConstIterRange<UInt16List, UInt16List> primSecIDIter(*oldPrimaryIDs, *oldSecondaryIDs);
        !primSecIDIter.empty(); ++primSecIDIter)
   {
      retVal = retVal && this->localTargetMapper.unmapTarget(*(primSecIDIter()->first));
      retVal = retVal && this->localTargetMapper.unmapTarget(*(primSecIDIter()->second));
   }

   return retVal;
}

/**
 * Find the TargetNumID of the next target which should be used for a MirrorBuddyGroup. A target
 * from the storage server with the most targets will be selected. It is possible to ignore the
 * targets of a storage server. The selected target is removed from the given TargetMapper.
 *
 * @param nodeNumIdToIgnore The NodeNumID to ignore in the search or 0 to disable the ignore feature
 * @param storagePoolId find only targets in this storagepool; might be (and defaults to)
 *        INVALID_POOL_ID; in which case it is ignored
 * @return the TargetNumID to use or 0 if no target found
 */
uint16_t MirrorBuddyGroupCreator::findNextTarget(NumNodeID nodeNumIdToIgnore,
   StoragePoolId storagePoolId)
{
   uint16_t retVal = 0;
   size_t maxNumTargets = 0;

   for (const auto& node : this->nodes->referenceAllNodes())
   {
      if (node->getNumID() == nodeNumIdToIgnore)
         continue;

      UInt16List targetList;

      UInt16List availableTargets;
      this->localTargetMapper.getTargetsByNode(node->getNumID(), availableTargets);

      // if a storage pool is given, filter the targets now
      if (storagePoolId != StoragePoolStore::INVALID_POOL_ID)
      {
         const UInt16Set allowedTargets = storagePoolStore->getPool(storagePoolId)->getTargets();
         // note: set_intersection needs sorted containers, thus we sort availableTargets first;
         // allowedTargets is a set, so it is sorted already
         availableTargets.sort();

         std::set_intersection(availableTargets.begin(), availableTargets.end(),
                               allowedTargets.begin(), allowedTargets.end(),
                               std::back_inserter(targetList));
      }
      else
      {
         // simply take available targets as target list
         targetList = std::move(availableTargets); // availableTargets won't be used anymore
      }


      if(targetList.size() > maxNumTargets)
      {
         maxNumTargets = targetList.size();
         retVal = targetList.front();
      }
   }

   this->localTargetMapper.unmapTarget(retVal);

   return retVal;
}

uint16_t MirrorBuddyGroupCreator::generateID(const UInt16List* usedMirrorBuddyGroups)
{
   const std::set<uint16_t> used(usedMirrorBuddyGroups->begin(), usedMirrorBuddyGroups->end());

   struct ops
   {
      static bool hasGap(uint16_t a, uint16_t b)
      {
         return a + 1 < b;
      }
   };

   if (used.empty())
      return 1;

   if (*used.rbegin() < 0xFFFF)
      return *used.rbegin() + 1;

   if (*used.begin() > 1)
      return 1;

   const auto gap = std::adjacent_find(used.begin(), used.end(), ops::hasGap);
   if (gap != used.end())
      return *gap + 1;

   return 0;
}

uint16_t MirrorBuddyGroupCreator::generateUniqueID(const TargetMapper* targetMapper,
   const UInt16List* usedMirrorBuddyGroups) const
{
   uint16_t newGroupID = 1;

   // search if the selected ID is in use
   bool idFound = false;
   UInt16ListConstIter groupIter = usedMirrorBuddyGroups->begin();
   for( ; groupIter !=  usedMirrorBuddyGroups->end(); groupIter++)
   {
      if(*groupIter == newGroupID)
      {
         idFound = true;
         break;
      }
   }

   if (idFound || (targetMapper && targetMapper->getNodeID(newGroupID) ) )
   {
      /*selected ID is in use already, so walk from there to find the next free ID. */
      for ( ;; )
      {
         newGroupID++;

         // check range... (avoid reserved value "0" as newNumID)
         if ( unlikely(!newGroupID || (newGroupID > MIRRORBUDDYGROUPMAPPER_MAX_GROUPIDS) ) )
         {
            newGroupID = 0;
            break;
         }

         // search if the selected ID is in use
         idFound = false;
         groupIter = usedMirrorBuddyGroups->begin();
         for( ; groupIter !=  usedMirrorBuddyGroups->end(); groupIter++)
         {
            if(*groupIter == newGroupID)
            {
               idFound = true;
               break;
            }
         }

         if (!idFound && targetMapper && !targetMapper->getNodeID(newGroupID))
            return newGroupID; // we found an ID that no other node uses
      }
   }

   return newGroupID;
}

/**
 * Check the size of the targets and prints a warning to the console if the size of all targets is
 * not equal.
 *
 * @param mapper TargetMapper with all storage targets of the system. For checking metadata nodes,
 *               the mapper just needs to contain the correct node IDs, the target IDs are not
 *               relevant (since metanodes can't have multiple targets)
 * @return false if the targets have a different size or a error occurred,
 *    true if all targets have the same size
 */
bool MirrorBuddyGroupCreator::checkSizeOfTargets() const
{
   bool retVal = true;

   int64_t spaceValue = 0;


   // Retrieve the statStoragePath information from all storage servers
   for (const auto& node : this->nodes->referenceAllNodes())
   {
      if (!retVal)
         break;

      UInt16List targets;
      this->systemTargetMapper.getTargetsByNode(node->getNumID(), targets);

      // Retrieve the statStoragePath information from the storage server
      for(UInt16ListIter targetIter = targets.begin(); targetIter != targets.end(); targetIter++)
      {
         int64_t freeSpace;
         int64_t totalSpace;
         int64_t freeInodes;
         int64_t totalInodes;

         const FhgfsOpsErr statRes = StorageTargetInfo::statStoragePath(*node, *targetIter,
            &freeSpace, &totalSpace, &freeInodes, &totalInodes);
         if(statRes != FhgfsOpsErr_SUCCESS)
         {
            const std::string logMessage("An error occurred when connecting to server: "
               + node->getNodeIDWithTypeStr() + " Error: "
               + boost::lexical_cast<std::string>(statRes));
            std::cerr << logMessage << std::endl;
            LOG(MIRRORING, ERR, logMessage);

            return false;
         }

         if(spaceValue == 0) // check of the first target
            spaceValue = totalSpace;
         else
         if(spaceValue != totalSpace)
         {
            retVal = false;
            break;
         }
      }
   }

   // report a warning, because the size of all targets is not equal
   if(!retVal)
   {
      const std::string logMessage("Target size differs. "
         "Automatic mode does not take this into account. "
         "It is recommended that all storage targets have the same size when using automatic mode."
         );
      std::cout << "[WARNING] " << logMessage << std::endl;
      LOG(MIRRORING, WARNING, logMessage);
   }

   return retVal;
}

/**
 * Checks which of the both targets is the best choice for the primary target. The storage server
 * with the lower primary target count is the best choice.
 *
 * @param primaryUsed A map with NodeNumIDs to counter of primary targets
 * @param inOutPrimaryID In/Out value for the primary Target/NodeNumID
 * @param inOutSecondaryID In/Out value for the secondary Target/NodeNumID
 */
void MirrorBuddyGroupCreator::selectPrimaryTarget(PrimaryTargetCounterMap* primaryUsed,
   uint16_t* inOutPrimaryID, uint16_t* inOutSecondaryID) const
{
   const NumNodeID primaryServerNumID = this->systemTargetMapper.getNodeID(*inOutPrimaryID);
   const NumNodeID secondaryServerNumID = this->systemTargetMapper.getNodeID(*inOutSecondaryID);

   if((*primaryUsed)[primaryServerNumID] > (*primaryUsed)[secondaryServerNumID])
   {
      const uint16_t tmpID = *inOutPrimaryID;
      *inOutPrimaryID = *inOutSecondaryID;
      *inOutSecondaryID = tmpID;

      (*primaryUsed)[secondaryServerNumID]++;
   }
   else
      (*primaryUsed)[primaryServerNumID]++;
}

/**
 * Generates MirrorBuddyGroups with the given targets, but do not create the MirrorBuddyGroups on
 * the management daemon
 *
 * @param outBuddyGroupIDs A list with all new selected MirrorBuddyGroupIDs
 * @param outBuddyGroups A list with all new selected MirrorBuddyGroups
 * @param usedMirrorBuddyGroupIDs A list with used MirrorBuddyGroupIDs
 * @return FhgfsOpsErr_SUCCESS when the selected MirrorBuddyGroups does not hurt any constraint,
 *    FhgfsOpsErr_INVAL at least one constraint is hurt, on error FhgfsOpsErr_...
 */
FhgfsOpsErr MirrorBuddyGroupCreator::generateMirrorBuddyGroups(UInt16List* outBuddyGroupIDs,
   MirrorBuddyGroupList* outBuddyGroups, const UInt16List* usedMirrorBuddyGroupIDs)
{
   FhgfsOpsErr retVal = FhgfsOpsErr_SUCCESS;

   size_t numTargets = this->localTargetMapper.getSize();
   if(numTargets == 0)
   { // check if unused targets exists
      const std::string logMessage(
         "No targets without a mirror group found. Mirror groups have not been changed.");
      std::cout << logMessage << std::endl;
      LOG(MIRRORING, ERR, logMessage);

      return FhgfsOpsErr_INTERNAL;
   }

   if(numTargets % 2 != 0)
   { // check for even number of targets
      const std::string logMessage(
         "Odd number of targets detected. One target left without a mirror group.");
      std::cout << "[WARNING] " << logMessage << std::endl;
      LOG(MIRRORING, WARNING, logMessage);

      retVal = FhgfsOpsErr_INVAL;
   }

   // check if all targets have the same size
   if(!checkSizeOfTargets() )
      retVal = FhgfsOpsErr_INVAL;

   // create a black list of MirrorBuddyGroupIDs, used MirrorBuddyGroupIDs + new MirrorBuddyGroupIDs
   UInt16List blackListGroupIDs;
   blackListGroupIDs.insert(blackListGroupIDs.begin(), usedMirrorBuddyGroupIDs->begin(),
      usedMirrorBuddyGroupIDs->end() );

   PrimaryTargetCounterMap primaryTargetCounter;

   while(numTargets > 1)
   { // requires at least two targets for a BuddyMirrorGroup
      uint16_t mirrorBuddyGroupID = 0;
      uint16_t primaryTarget = findNextTarget(NumNodeID(0) );

      StoragePoolId storagePoolId;
      if (nodeType == NODETYPE_Storage)
      {
         // get the storage pool of the primary target, because secondary target must be in the
         // same pool
         const StoragePoolPtr storagePool = storagePoolStore->getPool(primaryTarget);

         if (!storagePool)
         {
            // should actually never happen
            std::cerr << "Could not add buddy group: Requested primary target is not a member of a "
               "storage pool." << std::endl;
            return FhgfsOpsErr_INTERNAL;
         }

         storagePoolId = storagePool->getId();
      }
      else
      {
         storagePoolId = StoragePoolStore::INVALID_POOL_ID;
      }

      // get the node of the primary target, so we can try to find a different node for the
      // secondary target
      const NumNodeID ignoreID = this->systemTargetMapper.getNodeID(primaryTarget);
      uint16_t secondaryTarget = findNextTarget(ignoreID, storagePoolId);

      if(secondaryTarget)
      { // second target on a different server found ==> good case
         selectPrimaryTarget(&primaryTargetCounter, &primaryTarget, &secondaryTarget);
      }
      else
      { // all targets are located on the same server ==> bad case
         secondaryTarget = findNextTarget(NumNodeID(0), storagePoolId);
         if(!secondaryTarget)
         {
            const std::string logMessage("No second target for mirror group found.");
            std::cout << logMessage << std::endl;
            LOG(MIRRORING, ERR, logMessage);

            return FhgfsOpsErr_INTERNAL;
         }
         const std::string logMessage("Created new buddy group using targets " +
            StringTk::uintToStr(primaryTarget) + " and " + StringTk::uintToStr(secondaryTarget) +
            ", but both targets are located on the same server.");
         std::cout << "[WARNING] " << logMessage << std::endl;
         LOG(MIRRORING, WARNING, logMessage);

         retVal = FhgfsOpsErr_INVAL;
      }

      // if we have selected the owner of the root inode as secondary, just switch primary and
      // secondary to fix that.
      if (nodeType == NODETYPE_Meta && secondaryTarget == metaRoot->getID().val())
         std::swap(primaryTarget, secondaryTarget);

      if (this->cfgUnique)
         mirrorBuddyGroupID = generateUniqueID(&systemTargetMapper, &blackListGroupIDs);
      else
         mirrorBuddyGroupID = generateID(&blackListGroupIDs);

      outBuddyGroupIDs->push_back(mirrorBuddyGroupID);
      outBuddyGroups->push_back(MirrorBuddyGroup(primaryTarget, secondaryTarget) );
      blackListGroupIDs.push_back(mirrorBuddyGroupID);

      numTargets = this->localTargetMapper.getSize();
   }

   if(numTargets == 1)
   { // odd number of targets
      const uint16_t lastTarget = findNextTarget(NumNodeID(0) );
      const std::string logMessage("Storage target with targetNumID "
         + StringTk::uintToStr(lastTarget) + " unused, because no second target available.");
      std::cout << "[WARNING] " << logMessage << std::endl;
      LOG(MIRRORING, WARNING, logMessage);

      retVal = FhgfsOpsErr_INVAL;
   }

   return retVal;
}
