package com.airlock.iam.flow.shared.domain.service.cronto;

import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.store.exception.StoreFailureException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.common.domain.model.cronto.ExternalCrontoDeviceId;
import com.airlock.iam.core.domain.model.cronto.challenge.CrontoDeactivationChallenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoToken;
import com.airlock.iam.core.misc.util.cronto.CrontoUtils;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
import lombok.NonNull;
import org.apache.commons.lang3.Strings;

public final class CrontoDeviceManagementServiceImpl implements CrontoDeviceManagementService {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoDeviceManagementServiceImpl.class);
   @NonNull
   private final CrontoHandler handler;

   @Inject
   public CrontoDeviceManagementServiceImpl(@NonNull CrontoHandler handler) {
      if (handler == null) {
         throw new NullPointerException("handler is marked non-null but is null");
      }

      this.handler = handler;
   }

   @Override
   public CrontoDeviceToken getDeviceById(UserId userId, ExternalCrontoDeviceId id) throws EntityNotFoundException, StoreFailureException {
      this.ensuresUserHasCrontoAccount(userId);
      Optional<CrontoToken> token = Optional.ofNullable(this.handler.getTokenBySerialId(userId, id.getId()));
      if (token.isEmpty()) {
         throw new EntityNotFoundException("Cronto device with serial ID " + id + " not found");
      } else if (!(token.get() instanceof CrontoDeviceToken)) {
         throw new EntityNotFoundException("Cronto device token not found (wrong type): " + id);
      } else {
         return (CrontoDeviceToken)token.get();
      }
   }

   @Override
   public List<CrontoDeviceToken> getDevicesByUsername(UserId userId) throws EntityNotFoundException, StoreFailureException {
      this.ensuresUserHasCrontoAccount(userId);
      return this.handler.getAllDevicesForUser(userId);
   }

   @Override
   public void setDeviceLabel(UserId userId, ExternalCrontoDeviceId id, String newCustomName) {
      newCustomName = CrontoUtils.cleanDeviceName(newCustomName);
      CrontoDeviceToken crontoDevice = this.getDeviceById(userId, id);
      if (Strings.CS.equals(crontoDevice.getCustomName(), newCustomName)) {
         LOG.debug("Not updating label of device " + LogUtil.quote(crontoDevice.getDeviceId()) + " since it is already " + LogUtil.quote(newCustomName));
      } else {
         crontoDevice.setCustomName(newCustomName);
         this.persistToken(crontoDevice);
         LOG.info("Updated label of device" + LogUtil.quote(crontoDevice.getDeviceId()) + " to " + LogUtil.quote(newCustomName));
      }
   }

   @Override
   public void setDeviceEnabled(UserId userId, ExternalCrontoDeviceId id, boolean enabled) {
      this.ensuresUserHasCrontoAccount(userId);
      CrontoDeviceToken device = this.getDeviceById(userId, id);
      if (device.isActive() == enabled) {
         LOG.debug(
            "Not changing active flag of device " + LogUtil.quote(device.getDeviceId()) + " since it is already " + LogUtil.quote(Boolean.toString(enabled))
         );
      } else {
         device.setActive(enabled);
         this.persistToken(device);
         LOG.info("Changed state of device " + LogUtil.quote(device.getDeviceId()) + " to " + LogUtil.quote(enabled ? "enabled" : "disabled"));
      }
   }

   @Override
   public CrontoDeactivationChallenge deleteDevice(UserId userId, ExternalCrontoDeviceId id) {
      this.ensuresUserHasCrontoAccount(userId);
      CrontoDeviceToken device = this.getDeviceById(userId, id);

      try {
         return this.handler.deleteDevice(userId, device.getDeviceId());
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   @Override
   public boolean hasUserCrontoAccount(UserId userId) throws StoreFailureException {
      return this.handler.getAccountForUser(userId).isPresent();
   }

   @Override
   public void allowPushNotification(UserId userId, ExternalCrontoDeviceId id) {
      this.setPushNotificationAllowed(userId, id, true);
   }

   @Override
   public void disallowPushNotification(UserId userId, ExternalCrontoDeviceId id) {
      this.setPushNotificationAllowed(userId, id, false);
   }

   private void setPushNotificationAllowed(UserId userId, ExternalCrontoDeviceId id, boolean isAllowed) {
      this.ensuresUserHasCrontoAccount(userId);
      CrontoDeviceToken device = this.getDeviceById(userId, id);
      if (device.isPushNotificationAllowed() == isAllowed) {
         LOG.debug(
            "Not changing allowance flag of push notification for device "
               + LogUtil.quote(device.getDeviceId())
               + " since it is already "
               + LogUtil.quote(Boolean.toString(isAllowed))
         );
      } else {
         this.setPushNotificationAllowed(device, isAllowed);
         this.persistToken(device);
         LOG.info("Changed state of device " + LogUtil.quote(device.getDeviceId()) + " to " + LogUtil.quote(isAllowed ? "allowed" : "disallowed"));
      }
   }

   private void setPushNotificationAllowed(CrontoDeviceToken device, boolean isAllowed) {
      if (isAllowed) {
         device.allowPushNotification();
      } else {
         device.disallowPushNotification();
      }
   }

   private void ensuresUserHasCrontoAccount(UserId userId) {
      if (!this.hasUserCrontoAccount(userId)) {
         throw new EntityNotFoundException("User " + LogUtil.quote(userId) + " does not have a Cronto account");
      }
   }

   private void persistToken(CrontoToken token) {
      try {
         this.handler.getTokenDataProvider().updateToken(token.getDelegate());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }
}
