package com.airlock.iam.common.infrastructure.device.usage;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.infrastructure.db.DbInstantConverter;
import com.airlock.iam.base.misc.exception.Exceptions;
import com.airlock.iam.common.application.configuration.device.DeviceUsageRepositoryConfig;
import com.airlock.iam.common.domain.model.device.usage.DeviceUsage;
import com.airlock.iam.common.domain.model.device.usage.repository.DeviceUsageRepository;
import com.airlock.iam.core.infrastructure.db.JooqProvider;
import com.airlock.iam.database.infrastructure.JooqConfig;
import com.airlock.iam.database.infrastructure.JooqTransaction;
import com.airlock.iam.database.infrastructure.TenantId;
import com.airlock.iam.database.infrastructure.generated.schema.Tables;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.function.Supplier;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.SelectFieldOrAsterisk;

@LogicForConfig(DeviceUsageRepositoryConfig.class)
public final class DeviceUsageRepositoryImpl implements DeviceUsageRepository {
   private final Supplier<DSLContext> jooqDslProvider;
   private final TenantId tenantId;

   @Inject
   public DeviceUsageRepositoryImpl(JooqConfig jooqConfig) {
      this.jooqDslProvider = () -> JooqProvider.jooqProvider(jooqConfig).db();
      this.tenantId = jooqConfig.tenantId();
   }

   @Override
   public void insert(DeviceUsage deviceUsage) {
      Exceptions.wrapException(
         () -> JooqTransaction.jooqTransaction(this.db()).execute(db -> this.insertDeviceUsage(db, deviceUsage)),
         e -> new FailureException("Error inserting device usage entry", e)
      );
   }

   @Override
   public void update(DeviceUsage deviceUsage) {
      Exceptions.wrapException(
         () -> JooqTransaction.jooqTransaction(this.db()).execute(db -> this.updateDeviceUsage(db, deviceUsage)),
         e -> new FailureException("Error updating device usage entry", e)
      );
   }

   @Override
   public Optional<DeviceUsage> getDeviceUsage(UserId userId, String deviceType, String deviceId) {
      return Exceptions.wrapException(
         () -> this.getDeviceUsage(this.db(), userId, deviceType, deviceId), e -> new FailureException("Error fetching device usages", e)
      );
   }

   @Override
   public void deleteByUser(UserId userId) {
      Exceptions.wrapException(() -> this.deleteByUser(this.db(), userId), e -> new FailureException("Error deleting device usages", e));
   }

   @Override
   public int updateUsername(UserId fromUser, UserId toUser) {
      return Exceptions.wrapException(
         () -> this.updateUsername(this.db(), fromUser, toUser), e -> new FailureException("Error updating username for device usages", e)
      );
   }

   private int insertDeviceUsage(DSLContext db, DeviceUsage deviceUsage) {
      return db.insertInto(Tables.DEVICE_USAGE)
         .set(Tables.DEVICE_USAGE.USER_ID, deviceUsage.userId().getValue())
         .set(Tables.DEVICE_USAGE.DEVICE_TYPE, deviceUsage.deviceType())
         .set(Tables.DEVICE_USAGE.DEVICE_ID, deviceUsage.deviceId())
         .set(Tables.DEVICE_USAGE.TOTAL_USAGES, deviceUsage.totalUsages())
         .set(Tables.DEVICE_USAGE.FIRST_USAGE_DATE, DbInstantConverter.toNanoSecondsSinceEpoch(deviceUsage.firstUsedAt()))
         .set(Tables.DEVICE_USAGE.LATEST_USAGE_DATE, DbInstantConverter.toNanoSecondsSinceEpoch(deviceUsage.lastUsedAt()))
         .set(Tables.DEVICE_USAGE.TENANT_ID, this.tenantId.tenantId())
         .execute();
   }

   private int updateDeviceUsage(DSLContext db, DeviceUsage deviceUsage) {
      return db.update(Tables.DEVICE_USAGE)
         .set(Tables.DEVICE_USAGE.TOTAL_USAGES, deviceUsage.totalUsages())
         .set(Tables.DEVICE_USAGE.LATEST_USAGE_DATE, DbInstantConverter.toNanoSecondsSinceEpoch(deviceUsage.lastUsedAt()))
         .where(Tables.DEVICE_USAGE.USER_ID.eq(deviceUsage.userId().getValue()))
         .and(Tables.DEVICE_USAGE.DEVICE_TYPE.eq(deviceUsage.deviceType()))
         .and(Tables.DEVICE_USAGE.DEVICE_ID.eq(deviceUsage.deviceId()))
         .and(this.tenantMatches())
         .execute();
   }

   private void deleteByUser(DSLContext db, UserId userId) {
      db.deleteFrom(Tables.DEVICE_USAGE).where(Tables.DEVICE_USAGE.USER_ID.eq(userId.getValue())).and(this.tenantMatches()).execute();
   }

   private int updateUsername(DSLContext db, UserId fromUser, UserId toUser) {
      return db.update(Tables.DEVICE_USAGE)
         .set(Tables.DEVICE_USAGE.USER_ID, toUser.getValue())
         .where(Tables.DEVICE_USAGE.USER_ID.eq(fromUser.getValue()))
         .and(this.tenantMatches())
         .execute();
   }

   private Optional<DeviceUsage> getDeviceUsage(DSLContext db, UserId userId, String deviceType, String deviceId) {
      return db.select(new SelectFieldOrAsterisk[0])
         .from(Tables.DEVICE_USAGE)
         .where(Tables.DEVICE_USAGE.USER_ID.eq(userId.getValue()))
         .and(Tables.DEVICE_USAGE.DEVICE_TYPE.eq(deviceType))
         .and(Tables.DEVICE_USAGE.DEVICE_ID.eq(deviceId))
         .and(this.tenantMatches())
         .fetchOptional(DeviceUsageRepositoryImpl::mapDeviceUsage);
   }

   private static DeviceUsage mapDeviceUsage(Record record) {
      return new DeviceUsage(
         UserId.userId((String)record.get(Tables.DEVICE_USAGE.USER_ID)),
         (String)record.get(Tables.DEVICE_USAGE.DEVICE_TYPE),
         (String)record.get(Tables.DEVICE_USAGE.DEVICE_ID),
         (Integer)record.get(Tables.DEVICE_USAGE.TOTAL_USAGES),
         DbInstantConverter.toInstant((Long)record.get(Tables.DEVICE_USAGE.FIRST_USAGE_DATE)),
         DbInstantConverter.toInstant((Long)record.get(Tables.DEVICE_USAGE.LATEST_USAGE_DATE))
      );
   }

   private Condition tenantMatches() {
      return Tables.DEVICE_USAGE.TENANT_ID.eq(this.tenantId.tenantId());
   }

   private DSLContext db() {
      return this.jooqDslProvider.get();
   }
}
