/*
 * This file is part of the Meeds project (https://meeds.io/).
 * Copyright (C) 2020 - 2022 Meeds Association contact@meeds.io
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package io.meeds.tenant.provisioning.service;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.stereotype.Component;

import io.meeds.deeds.constant.ObjectNotFoundException;
import io.meeds.deeds.constant.TenantProvisioningStatus;
import io.meeds.deeds.constant.TenantStatus;
import io.meeds.deeds.elasticsearch.model.DeedTenant;
import io.meeds.deeds.service.BlockchainService;
import io.meeds.deeds.service.ListenerService;
import io.meeds.deeds.storage.DeedTenantManagerRepository;

@Component
public class TenantProvisioningService {
  private static final String                         KNOWN_TRANSACTIONS_PROP_NAME            = "knownTransactions";

  private static final Logger                         LOG                                     =
                                                          LoggerFactory.getLogger(TenantProvisioningService.class);

  public static final String                          DEED_EVENT_TENANT_CANCEL_COMMAND        = "deed.event.tenantCancelCommand";

  public static final String                          DEED_EVENT_TENANT_STOP_COMMAND_CREATED  =
                                                                                             "deed.event.tenantStopCommandCreated";

  public static final String                          DEED_EVENT_TENANT_START_COMMAND_CREATED =
                                                                                              "deed.event.tenantStartCommandCreated";

  public static final String                          DEED_EVENT_TENANT_COMPLETED_COMMAND     =
                                                                                          "deed.event.tenantCompletedCommand";

  public static final String                          DEED_EVENT_TENANT_STATUS_CHANGED        =
                                                                                       "deed.event.tenantStatusChanged";

  private static final List<TenantProvisioningStatus> PROVISIONING_PENDING_STATUSES           =
                                                                                    Arrays.asList(TenantProvisioningStatus.STOP_IN_PROGRESS,
                                                                                                  TenantProvisioningStatus.START_IN_PROGRESS);

  @Autowired
  private DeedTenantManagerRepository                 deedTenantManagerRepository;

  @Autowired
  private TaskIntegrationService                      taskIntegrationService;

  @Autowired
  private BlockchainService                           blockchainService;

  @Autowired
  private ListenerService                             listenerService;

  public List<DeedTenant> getTenants(TenantProvisioningStatus provisioningStatus, boolean includeCompleted, int limit) {
    PageRequest pageable = PageRequest.of(0, limit, Sort.by(Direction.DESC, "date"));
    if (includeCompleted) {
      return deedTenantManagerRepository.findByTenantProvisioningStatus(provisioningStatus, pageable);
    } else {
      return deedTenantManagerRepository.findByTenantProvisioningStatusAndCompletedIsFalse(provisioningStatus, pageable);
    }
  }

  public long getTenantsCount(TenantProvisioningStatus provisioningStatus, boolean includeCompleted) {
    if (includeCompleted) {
      return deedTenantManagerRepository.countByTenantProvisioningStatus(provisioningStatus);
    } else {
      return deedTenantManagerRepository.countByTenantProvisioningStatusAndCompletedIsFalse(provisioningStatus);
    }
  }

  /**
   * Sends a Start Tenant Task for ITOP to start a new Tenant
   * 
   * @param  nftId                   NFT Deed ID
   * @param  transactionHash         Blockchain Transaction Hash to command
   *                                   provisioning
   * @param  confirmed               whether provisioning transaction has been
   *                                   confirmed or not
   * @throws ObjectNotFoundException When Deed NFT with designated ID wasn't
   *                                   found
   */
  public void startTenant(long nftId, String transactionHash, boolean confirmed) throws ObjectNotFoundException {
    LOG.debug("A [startTenant] command has been SENT for nftId='{}' with transaction '{}' (confirmed='{}')",
              nftId,
              transactionHash,
              confirmed);
    DeedTenant deedTenant = null;
    try {
      deedTenant = deedTenantManagerRepository.findById(nftId).orElse(new DeedTenant());
      deedTenant.setNftId(nftId);
      deedTenant.setStartupTransactionHash(transactionHash);
      deedTenant.setShutdownTransactionHash(null);
      deedTenant.setCompleted(false);
      setDeedNftProperties(deedTenant);
      setTenantProvisioningStatus(deedTenant, true, confirmed);
      setDeedProvisioningTransaction(deedTenant, transactionHash);
      saveDeedTenant(deedTenant);

      taskIntegrationService.saveTask(deedTenant, transactionHash, true, confirmed);

      LOG.debug("The [startTenant] command task has been CREATED for nftId='{}' with transaction '{}' (confirmed='{}')",
                nftId,
                transactionHash,
                confirmed);
    } catch (Exception e) {// NOSONAR
      LOG.debug("The [startTenant] command task has NOT BEEN SAVED for nftId='{}' with transaction '{}' (confirmed='{}'). Error message='{}'",
                nftId,
                transactionHash,
                confirmed,
                e.getMessage());
      throw e;
    } finally {
      if (deedTenant != null) {
        saveDeedTenant(deedTenant);
        listenerService.publishEvent(DEED_EVENT_TENANT_START_COMMAND_CREATED, deedTenant);
      }
    }
  }

  /**
   * Sends a Stop Tenant Task for ITOP to undeploy existing Tenant
   * 
   * @param  nftId                   NFT Deed ID
   * @param  transactionHash         Blockchain Transaction Hash to command
   *                                   provisioning
   * @param  confirmed               whether provisioning transaction has been
   *                                   confirmed or not
   * @throws ObjectNotFoundException When Deed NFT with designated ID wasn't
   *                                   found
   */
  public void stopTenant(long nftId, String transactionHash, boolean confirmed) throws ObjectNotFoundException {
    LOG.debug("A [stopTenant] command has been [SENT] for nftId='{}' with transaction '{}' (confirmed='{}')",
              nftId,
              transactionHash,
              confirmed);
    DeedTenant deedTenant = null;
    try {
      deedTenant = deedTenantManagerRepository.findById(nftId).orElse(new DeedTenant());
      deedTenant.setNftId(nftId);
      deedTenant.setShutdownTransactionHash(transactionHash);
      deedTenant.setStartupTransactionHash(null);
      deedTenant.setCompleted(false);
      setDeedNftProperties(deedTenant);
      setTenantProvisioningStatus(deedTenant, false, confirmed);
      setDeedProvisioningTransaction(deedTenant, transactionHash);
      saveDeedTenant(deedTenant);

      taskIntegrationService.saveTask(deedTenant, transactionHash, false, confirmed);

      LOG.debug("The [stopTenant] command task has been [CREATED] for nftId='{}' with transaction '{}' (confirmed='{}')",
                nftId,
                transactionHash,
                confirmed);
    } catch (Exception e) {// NOSONAR
      LOG.debug("The [stopTenant] command task has [NOT BEEN SAVED] for nftId='{}' with transaction '{}' (confirmed='{}'). Error message='{}'",
                nftId,
                transactionHash,
                confirmed,
                e.getMessage());
      throw e;
    } finally {
      if (deedTenant != null) {
        saveDeedTenant(deedTenant);
        listenerService.publishEvent(DEED_EVENT_TENANT_STOP_COMMAND_CREATED, deedTenant);
      }
    }
  }

  /**
   * Cancels Provisioning Task because the transaction failed for example
   * 
   * @param nftId           NFT ID
   * @param transactionHash Provisioning command Transaction Hash
   */
  public void cancelTenantProvisioning(long nftId, String transactionHash) {
    LOG.debug("A [cancelTransaction] command has been [SENT] for nftId='{}' with transaction '{}')", nftId, transactionHash);
    DeedTenant deedTenant = deedTenantManagerRepository.findById(nftId).orElse(null);
    if (deedTenant != null) {
      if (blockchainService.isDeedStarted(nftId)) {
        deedTenant.setTenantProvisioningStatus(TenantProvisioningStatus.START_CONFIRMED);
      } else {
        deedTenant.setTenantProvisioningStatus(TenantProvisioningStatus.STOP_CONFIRMED);
      }
      deedTenant.setCompleted(true);
      saveDeedTenant(deedTenant);

      try {
        taskIntegrationService.cancelTask(deedTenant, transactionHash);
        LOG.debug("The [cancelTransaction] command task has been [CLOSED] for nftId='{}' with transaction '{}'",
                  nftId,
                  transactionHash);
      } catch (Exception e) {// NOSONAR
        LOG.debug("The [cancelTransaction] command task has [NOT BEEN CLOSED] for nftId='{}' with transaction '{}'. Error message='{}'",
                  nftId,
                  transactionHash,
                  e.getMessage());
        throw e;
      } finally {
        listenerService.publishEvent(DEED_EVENT_TENANT_CANCEL_COMMAND, deedTenant);
      }
    }
  }

  public void saveDeedTenantStatus(long nftId, TenantStatus tenantStatus) throws ObjectNotFoundException {
    DeedTenant deedTenant = deedTenantManagerRepository.findById(nftId).orElse(null);
    if (deedTenant == null) {
      throw new ObjectNotFoundException("NFT with id " + nftId + " wasn't found");
    }
    setTenantStatus(deedTenant, tenantStatus);
    deedTenantManagerRepository.save(deedTenant);
    listenerService.publishEvent(DEED_EVENT_TENANT_STATUS_CHANGED, deedTenant);
  }

  public void changeDeedTenantCompletedStatus(long taskId, boolean completed) {
    DeedTenant deedTenant = deedTenantManagerRepository.findByTaskId(String.valueOf(taskId)).findFirst().orElse(null);
    if (deedTenant != null && deedTenant.isCompleted() != completed) {
      deedTenant.setCompleted(completed);
      if (completed && deedTenant.getTenantProvisioningStatus() != null) {
        if (deedTenant.getTenantProvisioningStatus().isStart()) {
          setTenantStatus(deedTenant, TenantStatus.DEPLOYED);
        } else {
          setTenantStatus(deedTenant, TenantStatus.UNDEPLOYED);
        }
      }
      deedTenantManagerRepository.save(deedTenant);
      listenerService.publishEvent(DEED_EVENT_TENANT_COMPLETED_COMMAND, deedTenant);
    }
  }

  public boolean isTransactionAlreadyConfirmed(String transactionHash, long nftId) {
    DeedTenant deedTenant = deedTenantManagerRepository.findById(nftId).orElse(null);
    boolean newTenant = deedTenant == null || deedTenant.getTenantProvisioningStatus() == null;
    if (newTenant) {
      return false;
    }
    boolean transactionHashIsKnown = StringUtils.equalsIgnoreCase(transactionHash, deedTenant.getStartupTransactionHash())
        || StringUtils.equalsIgnoreCase(transactionHash, deedTenant.getShutdownTransactionHash());
    if (transactionHashIsKnown) {
      return deedTenant.getTenantProvisioningStatus().isConfirmed();
    } else {
      String knownTransactions =
                               deedTenant.getProperties() == null ? null
                                                                  : deedTenant.getProperties().get(KNOWN_TRANSACTIONS_PROP_NAME);
      return StringUtils.containsIgnoreCase(knownTransactions, transactionHash);
    }
  }

  public boolean isManager(String userName) {
    return getManagers().contains(userName);
  }

  public List<DeedTenant> getPendingTransactions() {
    return deedTenantManagerRepository.findByTenantProvisioningStatusIn(PROVISIONING_PENDING_STATUSES);
  }

  public DeedTenant saveDeedTenant(DeedTenant minedDeedTenantTransaction) {
    return deedTenantManagerRepository.save(minedDeedTenantTransaction);
  }

  public boolean isDeedExists(long nftId) {
    return deedTenantManagerRepository.existsById(nftId);
  }

  public Set<String> getManagers() {
    return taskIntegrationService.getProjectManagers();
  }

  public String getManagerEmail(long nftId) throws ObjectNotFoundException {
    DeedTenant deedTenant = deedTenantManagerRepository.findById(nftId)
                                                       .orElseThrow(() -> new ObjectNotFoundException("Deed Tenant not found"));
    return deedTenant.getManagerEmail();
  }

  private void setTenantProvisioningStatus(DeedTenant deedTenant, boolean start, boolean confirmed) {
    if (start) {
      if (confirmed) {
        setTenantProvisioningStatus(deedTenant, TenantProvisioningStatus.START_CONFIRMED);
      } else {
        setTenantProvisioningStatus(deedTenant, TenantProvisioningStatus.START_IN_PROGRESS);
      }
    } else {
      if (confirmed) {
        setTenantProvisioningStatus(deedTenant, TenantProvisioningStatus.STOP_CONFIRMED);
      } else {
        setTenantProvisioningStatus(deedTenant, TenantProvisioningStatus.STOP_IN_PROGRESS);
      }
    }
  }

  private void setTenantProvisioningStatus(DeedTenant deedTenant, TenantProvisioningStatus newStatus) {
    if (newStatus != deedTenant.getTenantProvisioningStatus()) {
      deedTenant.setTenantProvisioningStatus(newStatus);
      deedTenant.setDate(LocalDateTime.now(ZoneOffset.UTC));
    }
  }

  private void setTenantStatus(DeedTenant deedTenant, TenantStatus tenantStatus) {
    deedTenant.setTenantStatus(tenantStatus);
    deedTenant.setDate(LocalDateTime.now(ZoneOffset.UTC));
  }

  private void setDeedNftProperties(DeedTenant deedTenant) throws ObjectNotFoundException {
    try {
      if (deedTenant.getCardType() < 0) {
        short cardType = blockchainService.getDeedCardType(deedTenant.getNftId());
        deedTenant.setCardType(cardType);
      }
    } catch (ObjectNotFoundException e) {
      throw e;
    } catch (Exception e) {
      LOG.warn("Error while retieving NFT ID {} card Type. Ignore for now.", deedTenant.getNftId());
    }
    try {
      if (deedTenant.getCityIndex() < 0) {
        short cityIndex = blockchainService.getDeedCityIndex(deedTenant.getNftId());
        deedTenant.setCityIndex(cityIndex);
      }
    } catch (ObjectNotFoundException e) {
      throw e;
    } catch (Exception e) {
      LOG.warn("Error while retieving NFT ID {} city. Ignore for now.", deedTenant.getNftId());
    }
  }

  private void setDeedProvisioningTransaction(DeedTenant deedTenant, String transactionHash) {
    if (deedTenant.getProperties() == null) {
      deedTenant.setProperties(new HashMap<>());
    }
    String knownTransactions = deedTenant.getProperties().get(KNOWN_TRANSACTIONS_PROP_NAME);
    if (StringUtils.isBlank(knownTransactions)) {
      deedTenant.getProperties().put(KNOWN_TRANSACTIONS_PROP_NAME, transactionHash);
    } else {
      Set<String> knownTransactionsSet = new HashSet<>(Arrays.asList(knownTransactions.split(",")));
      knownTransactionsSet.add(transactionHash);
      deedTenant.getProperties().put(KNOWN_TRANSACTIONS_PROP_NAME, StringUtils.join(knownTransactionsSet, ","));
    }
  }

}
