package com.airlock.iam.common.infrastructure.usertrail;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
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.domain.model.usertrail.UserTrailLogRepository;
import com.airlock.iam.core.infrastructure.db.JooqProvider;
import com.airlock.iam.database.infrastructure.JooqTransaction;
import com.airlock.iam.database.infrastructure.TenantId;
import com.airlock.iam.database.infrastructure.generated.schema.Tables;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.UserTrailLogRecord;
import com.airlock.iam.log.usertrail.UserTrailLog;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.DeleteReturningStep;
import org.jooq.Record;
import org.jooq.SQLDialect;
import org.jooq.Select;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.impl.DSL;

public final class UserTrailLogRepositoryImpl implements UserTrailLogRepository {
   private static final String UNSET_REQUEST_ID = "no_request_id";
   private static final String UNSET_SESSION_ID = "no_session_id";
   private static final String UNSET_CLIENT_IP = "no_client_ip";
   private final Supplier<DSLContext> jooqDslProvider;
   private final TenantId tenantId;

   @Inject
   public UserTrailLogRepositoryImpl(UserTrailLogSettings settings) {
      this.jooqDslProvider = () -> JooqProvider.jooqProvider(settings.jooqConfig()).db();
      this.tenantId = settings.jooqConfig().tenantId();
   }

   @Override
   public int insert(UserTrailLog userTrailLogEntry) {
      return this.insert(List.of(userTrailLogEntry));
   }

   @Override
   public int insert(List<UserTrailLog> userTrailLogEntries) {
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db()).executeAndReturn(db -> this.insertUserTrailLog(db, userTrailLogEntries)),
         e -> new FailureException(String.format("Error inserting '%s': %s", userTrailLogEntries, e.getMessage()), e)
      );
   }

   private int insertUserTrailLog(DSLContext db, List<UserTrailLog> userTrailLogs) {
      return db.insertInto(Tables.USER_TRAIL_LOG)
         .columns(
            Tables.USER_TRAIL_LOG.CREATED_AT,
            Tables.USER_TRAIL_LOG.LOG_LEVEL,
            Tables.USER_TRAIL_LOG.TARGET_ID,
            Tables.USER_TRAIL_LOG.AUTHENTEE_ID,
            Tables.USER_TRAIL_LOG.AUTHENTEE_PROVIDED_ID,
            Tables.USER_TRAIL_LOG.MESSAGE,
            Tables.USER_TRAIL_LOG.CONFIGURATION_CONTEXT,
            Tables.USER_TRAIL_LOG.REQUEST_ID,
            Tables.USER_TRAIL_LOG.SESSION_ID,
            Tables.USER_TRAIL_LOG.CLIENT_IP,
            Tables.USER_TRAIL_LOG.TENANT_ID
         )
         .valuesOfRows(
            userTrailLogs.stream()
               .map(
                  userTrailLog -> DSL.row(
                     DbInstantConverter.toNanoSecondsSinceEpoch(userTrailLog.createdAt()),
                     userTrailLog.logLevel(),
                     userTrailLog.targetId(),
                     userTrailLog.authenteeId(),
                     userTrailLog.authenteeProvidedId(),
                     userTrailLog.message(),
                     userTrailLog.configurationContext(),
                     unwrapRequestId(userTrailLog.requestId()),
                     unwrapSessionId(userTrailLog.sessionId()),
                     unwrapClientIp(userTrailLog.clientIp()),
                     this.tenantId.tenantId()
                  )
               )
               .toList()
         )
         .execute();
   }

   private static String unwrapRequestId(Optional<String> maybeRequestId) {
      return maybeRequestId.orElse("no_request_id");
   }

   private static String unwrapSessionId(Optional<String> maybeSessionId) {
      return maybeSessionId.orElse("no_session_id");
   }

   private static String unwrapClientIp(Optional<String> maybeClientIp) {
      return maybeClientIp.orElse("no_client_ip");
   }

   @Override
   public List<UserTrailLog> findBy(UserId userId, int limit, int offset) {
      if (limit <= 0) {
         throw new IllegalArgumentException("Zero or negative limit in user trail log query.");
      } else if (offset < 0) {
         throw new IllegalArgumentException("Negative offset in user trail log query.");
      } else {
         return Exceptions.wrapException(
            () -> this.findUserTrailLogs(userId, limit, offset).fetch(UserTrailLogRepositoryImpl::mapUserTrailLog),
            e -> new FailureException(String.format("Error finding user trail log for user '%s': %s", userId, e.getMessage()), e)
         );
      }
   }

   private Select<Record> findUserTrailLogs(UserId userId, int limit, int offset) {
      return this.db()
         .select(new SelectFieldOrAsterisk[0])
         .from(Tables.USER_TRAIL_LOG)
         .where(userMatch(userId))
         .and(this.tenantMatches())
         .orderBy(Tables.USER_TRAIL_LOG.CREATED_AT.desc())
         .limit(limit)
         .offset(offset);
   }

   private static UserTrailLog mapUserTrailLog(Record record) {
      return UserTrailLog.builder()
         .createdAt(DbInstantConverter.toInstant((Long)record.get(Tables.USER_TRAIL_LOG.CREATED_AT)))
         .logLevel((String)record.get(Tables.USER_TRAIL_LOG.LOG_LEVEL))
         .targetId((String)record.get(Tables.USER_TRAIL_LOG.TARGET_ID))
         .authenteeId((String)record.get(Tables.USER_TRAIL_LOG.AUTHENTEE_ID))
         .authenteeProvidedId((String)record.get(Tables.USER_TRAIL_LOG.AUTHENTEE_PROVIDED_ID))
         .message((String)record.get(Tables.USER_TRAIL_LOG.MESSAGE))
         .configurationContext((String)record.get(Tables.USER_TRAIL_LOG.CONFIGURATION_CONTEXT))
         .requestId(removeInternalMarkerOnRequestId((String)record.get(Tables.USER_TRAIL_LOG.REQUEST_ID)))
         .sessionId(removeInternalMarkerOnSessionId((String)record.get(Tables.USER_TRAIL_LOG.SESSION_ID)))
         .clientIp(removeInternalMarkerOnClientIp((String)record.get(Tables.USER_TRAIL_LOG.CLIENT_IP)))
         .build();
   }

   private static String removeInternalMarkerOnRequestId(String requestId) {
      return requestId.equals("no_request_id") ? null : requestId;
   }

   private static String removeInternalMarkerOnSessionId(String sessionId) {
      return sessionId.equals("no_session_id") ? null : sessionId;
   }

   private static String removeInternalMarkerOnClientIp(String clientIp) {
      return clientIp.equals("no_client_ip") ? null : clientIp;
   }

   @Override
   public int deleteOlderThan(Duration retainUserTrailLogPeriod, int batchSize) {
      if (batchSize <= 0) {
         throw new IllegalArgumentException(String.format("Require positive batchSize to delete logs (was %s).", batchSize));
      }

      Instant deletionCutoffTime = calculateDeletionCutoffTime(retainUserTrailLogPeriod);
      return Exceptions.wrapException(
         () -> (Integer)JooqTransaction.jooqTransaction(this.db())
            .executeAndReturn(db -> this.deleteOlderThan(db, logCreatedBeforeMatch(deletionCutoffTime), batchSize)),
         e -> new FailureException(String.format("Error while deleting user trail logs older than '%s': %s", deletionCutoffTime, e.getMessage()), e)
      );
   }

   private int deleteOlderThan(DSLContext db, Condition whereClause, int batchSize) {
      DeleteConditionStep<UserTrailLogRecord> query = db.deleteFrom(Tables.USER_TRAIL_LOG).where(whereClause);
      return this.addLimit(query, batchSize, db).execute();
   }

   private DeleteReturningStep<?> addLimit(DeleteConditionStep<?> query, int batchSize, DSLContext db) {
      return (DeleteReturningStep<?>)(db.dialect().family() == SQLDialect.ORACLE ? query.and(DSL.rownum().lessOrEqual(batchSize)) : query.limit(batchSize));
   }

   @Override
   public int countOlderThan(Duration retainUserTrailLogPeriod) {
      Instant deletionCutoffTime = calculateDeletionCutoffTime(retainUserTrailLogPeriod);
      return Exceptions.wrapException(
         () -> this.fetchNumberOfLogEntriesToDelete(this.db(), logCreatedBeforeMatch(deletionCutoffTime)),
         e -> new FailureException(String.format("Error getting number of expired user trail logs: %s", e.getMessage()), e)
      );
   }

   private int fetchNumberOfLogEntriesToDelete(DSLContext db, Condition whereClause) {
      return (Integer)Objects.requireNonNullElse((Integer)db.selectCount().from(Tables.USER_TRAIL_LOG).where(whereClause).fetchOne(DSL.count()), 0);
   }

   private static Instant calculateDeletionCutoffTime(Duration duration) {
      return Instant.now().minus(duration);
   }

   private static Condition userMatch(UserId userId) {
      return Tables.USER_TRAIL_LOG.TARGET_ID.eq(userId.getValue());
   }

   private static Condition logCreatedBeforeMatch(Instant deletionCutoffTime) {
      return Tables.USER_TRAIL_LOG.CREATED_AT.lt(DbInstantConverter.toNanoSecondsSinceEpoch(deletionCutoffTime));
   }

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

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