package com.airlock.iam.servicecontainer.app.application.configuration.task;

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.core.misc.persistency.CredentialPersister;
import com.airlock.iam.core.misc.renderer.CredentialSecretGenerator;
import com.airlock.iam.core.misc.renderer.UserCredentialBean;
import com.airlock.iam.core.misc.util.password.generator.PasswordRendererException;
import com.airlock.iam.core.misc.util.report.ReportRendererException;
import com.airlock.iam.core.misc.util.report.aggregation.AggregateReport;
import com.airlock.iam.core.misc.util.report.aggregation.ReportData;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.audit.AuditLogger;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.LongProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.servicecontainer.api.application.configuration.task.TaskConfig;
import com.airlock.iam.servicecontainer.api.domain.model.task.AbstractTask;
import com.airlock.iam.servicecontainer.api.domain.model.task.Task;
import com.airlock.iam.servicecontainer.app.application.configuration.task.token.TokenCleanup;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.time.FastDateFormat;

public abstract class AbstractSecretStringBatchTask extends AbstractTask implements TaskConfig {
   private static final long HOURS_PER_DAY = 24L;
   private static final long MINUTES_PER_HOUR = 60L;
   private static final long SECONDS_PER_MINUTE = 60L;
   private static final long MILLIS_PER_SECOND = 1000L;
   private static final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("yyyy/MM/dd HH:mm:ss");
   private static final ELogger LOG = ELoggerFactory.getLogger(AbstractSecretStringBatchTask.class);
   @LongProperty(
      description = "In order to avoid sending more than one credential to a user at the same time, this task inspects the delivery times of other credentials of the same user. The value of this property indicates the minimum number of days between the latest delivery of another token and the generation of a secret.<p>Setting this property to zero (0) disables this feature. </p>",
      optional = true,
      minValue = 0L,
      maxValue = Long.MAX_VALUE,
      exampleValues = {0L, 3L, 5L},
      defaultValue = 0L
   )
   private long deliverySecurityGapDays;
   @PluginProperty(
      description = "Optional property to describe an aggregate report over all generated reports in a batch. If none is configured, no aggregate report will be generated.",
      optional = true
   )
   private AggregateReport aggregateReport;
   @PluginProperty(description = "Allows the configuration of settings for the generation of the credential secret reports.", optional = false)
   private CredentialSecretGenerator credentialSecretGenerator;
   @PluginListProperty(description = "Allows the configuration of settings to remove tokens during the batch task.", optional = true)
   private List<TokenCleanup> tokenCleanupConfigs;
   private AbstractSecretStringBatchTask.ConfigBean config;

   protected AbstractSecretStringBatchTask(
      long deliverySecurityGapDays,
      AggregateReport aggregateReport,
      CredentialSecretGenerator credentialSecretGenerator,
      List<TokenCleanup> tokenCleanupConfigs
   ) {
      this.deliverySecurityGapDays = deliverySecurityGapDays;
      this.aggregateReport = aggregateReport;
      this.credentialSecretGenerator = credentialSecretGenerator;
      this.tokenCleanupConfigs = tokenCleanupConfigs;
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      AbstractSecretStringBatchTask.ConfigBean cb = new AbstractSecretStringBatchTask.ConfigBean();
      synchronized (cb) {
         if (this.deliverySecurityGapDays == 0L) {
            cb.deliverySecurityGapMillis = 0L;
         } else {
            cb.deliverySecurityGapMillis = this.deliverySecurityGapDays * 24L * 60L * 60L * 1000L;
         }

         cb.isHashValueBinary = this.credentialSecretGenerator.isHashValueBinary();
         this.config = cb;
      }
   }

   @VisibleForTesting
   void setCredentialSecretGenerator(CredentialSecretGenerator credentialSecretGenerator) {
      this.credentialSecretGenerator = credentialSecretGenerator;
   }

   @VisibleForTesting
   void setTokenCleanupConfigs(List<TokenCleanup> tokenCleanupConfigs) {
      this.tokenCleanupConfigs = tokenCleanupConfigs;
   }

   protected CredentialSecretGenerator getCredentialSecretGenerator() {
      return this.credentialSecretGenerator;
   }

   @Override
   public void executeInternal() throws RuntimeException {
      long startTime = System.currentTimeMillis();
      List<ReportData> reports = Lists.newArrayList();
      AbstractSecretStringBatchTask.ConfigBean currentConfig = this.config;

      try {
         LOG.info(this.getLogType(), "Password batch task starts.");

         List<String> uids;
         try {
            LOG.debug("Getting all user ids for Password batch task");
            uids = this.getAllUserIds();
         } catch (PersisterException e) {
            LOG.error(this.getLogType(), "Error getting all user ids. Aborting batch job.");
            throw new RuntimeException("Error getting all user ids. Aborting batch job.", e);
         }

         int numOfUsers = uids.size();
         int numOfHandled = 0;
         LOG.debug("Found " + numOfUsers + " user ids for password batch task");

         for (String uid : uids) {
            if (this.checkShouldStop()) {
               LOG.info(
                  this.getLogType(),
                  "Password batch task ends incomplete, because of a please-stop request. progress = "
                     + this.getProgressPercentage()
                     + "% ("
                     + numOfHandled
                     + " of "
                     + numOfUsers
                     + " users completed."
               );
               return;
            }

            try {
               Optional<ReportData> reportData = this.handleUid(currentConfig, uid, startTime);
               if (reportData.isPresent()) {
                  reports.add(reportData.get());
               }
            } catch (PersisterException e) {
               LOG.error(this.getLogType(), "Error in Password batch task while handling uid \"" + uid + "\" (" + e.getMessage() + "). Skipping tan user. ", e);
            }

            numOfHandled++;
            this.setProgressPercentage(numOfHandled * 100 / numOfUsers);
         }

         this.setProgressPercentage(100);
      } finally {
         try {
            this.handleAggregateReport(reports);
         } catch (ReportRendererException e) {
            LOG.error(this.getLogType(), "Error in Password batch task while rendering aggregate report (" + e.getMessage() + ").", e);
         } catch (IOException e) {
            LOG.error(this.getLogType(), "Error in Password batch task while generating aggregate report (" + e.getMessage() + ").", e);
         }
      }
   }

   private void handleAggregateReport(List<ReportData> reports) throws ReportRendererException, IOException {
      if (this.aggregateReport != null && reports != null && !reports.isEmpty()) {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         this.aggregateReport.renderReport(reports, bos);
         this.credentialSecretGenerator.writeToFile(bos, this.aggregateReport.generateFileName());
      }
   }

   protected abstract List<String> getAllUserIds() throws PersisterException;

   @VisibleForTesting
   Optional<ReportData> handleUid(AbstractSecretStringBatchTask.ConfigBean currentConfig, String uid, long now) throws PersisterException {
      try {
         Objects.requireNonNull(uid);

         UserCredentialBean userBean;
         try {
            userBean = this.getUserCredentialData(uid);
         } catch (NotFoundException e) {
            LOG.warn(
               this.getLogType(),
               "The user/credential with uid \""
                  + uid
                  + "\" could not be found in the batch job. User might have been deleted during batch job. Skipping this user"
            );
            return Optional.empty();
         }

         if (userBean.isInvalid()) {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Skipping user structure with uid \"" + uid + "\" in batch task because: user is invalid");
            }

            return Optional.empty();
         }

         boolean generateNewSecret = false;
         if (userBean.getOrderFlag()) {
            AuditLogger.log(
               AbstractSecretStringBatchTask.class,
               this.getLogType(),
               "Planning to generate new secret for user with uid \"" + uid + "\" because order-flag is set"
            );
            generateNewSecret = true;
         }

         if (generateNewSecret) {
            Date[] otherCredentialsDeliveryDates = userBean.getOtherCredentialsDeliveryDates();
            if (currentConfig.getDeliverySecurityGapMillis() > 0L && otherCredentialsDeliveryDates != null && otherCredentialsDeliveryDates.length > 0) {
               for (Date otherDate : otherCredentialsDeliveryDates) {
                  if (otherDate != null && otherDate.getTime() + currentConfig.getDeliverySecurityGapMillis() > now) {
                     AuditLogger.log(
                        AbstractSecretStringBatchTask.class,
                        this.getLogType(),
                        "Not generating new secret for user with uid  \""
                           + uid
                           + "\" because the delivery date of some other credential ("
                           + DATE_FORMAT.format(otherDate)
                           + ") is too close (Delivery Security Gap)"
                     );
                     generateNewSecret = false;
                     break;
                  }
               }
            }
         }

         if (generateNewSecret) {
            if (this.checkShouldStop()) {
               return Optional.empty();
            }

            ReportData reportData = this.credentialSecretGenerator.renderSecretToReport(userBean, now);
            userBean.resetOrder();
            userBean.store();
            this.handleSessionCleanup(uid);
            return Optional.of(reportData);
         }
      } catch (PasswordRendererException e) {
         LOG.warn(this.getLogType(), "Error rendering password for uid \"" + uid + "\". Password not generated or rendered.", e);
      } catch (IOException e) {
         LOG.warn(this.getLogType(), "Error storing rendered password for uid \"" + uid + "\". Password not generated or rendered.", e);
      }

      return Optional.empty();
   }

   @VisibleForTesting
   void handleSessionCleanup(String uid) {
      this.tokenCleanupConfigs.forEach(t -> t.handlePasswordChange(UserId.userId(uid)));
   }

   protected abstract UserCredentialBean getUserCredentialData(String var1) throws PersisterException;

   public AbstractSecretStringBatchTask.ConfigBean getConfig() {
      return this.config;
   }

   @Override
   public String getThreadName() {
      return "AbstractSecretStringBatchTask";
   }

   @Override
   public Task getLogic() {
      return this;
   }

   @SuppressFBWarnings(justification = "generated code")
   public AbstractSecretStringBatchTask() {
   }

   protected static class ConfigBean {
      private CredentialPersister credentialPersister;
      private long deliverySecurityGapMillis;
      private boolean isHashValueBinary;

      protected void setCredentialPersister(CredentialPersister credentialPersister) {
         this.credentialPersister = credentialPersister;
      }

      protected void setDeliverySecurityGapMillis(long deliverySecurityGapMillis) {
         this.deliverySecurityGapMillis = deliverySecurityGapMillis;
      }

      protected void setHashValueBinary(boolean hashValueBinary) {
         this.isHashValueBinary = hashValueBinary;
      }

      protected long getDeliverySecurityGapMillis() {
         return this.deliverySecurityGapMillis;
      }

      protected CredentialPersister getCredentialPersister() {
         return this.credentialPersister;
      }

      protected boolean isHashValueBinary() {
         return this.isHashValueBinary;
      }
   }
}
