package com.airlock.iam.techclient.infrastructure.repository.principal;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.identity.Identity;
import com.airlock.iam.base.infrastructure.db.DbTimestamps;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.locking.LockInfo;
import com.airlock.iam.database.infrastructure.TenantId;
import com.airlock.iam.database.infrastructure.generated.schema.tables.ApiKeyTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.PrincipalTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.SubscriptionTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.PrincipalRecord;
import com.airlock.iam.techclient.api.domain.model.TechnicalClient;
import java.time.LocalDateTime;
import java.util.Optional;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.InsertSetMoreStep;
import org.jooq.Record;
import org.jooq.Sequence;

public final class SharedPrincipalQueries {
   public static final FailureException CLIENT_NOT_FOUND_EXCEPTION = new FailureException("client not found");
   public static final FailureException CLIENT_READBACK_EXCEPTION = new FailureException("could not read-back client");

   public static Integer insertPrincipal(Optional<Sequence<Integer>> principalSequence, TenantId tenantId, DSLContext db, TechnicalClient client) {
      return startPrincipalInsert(principalSequence, tenantId, db, client)
         .returningResult(PrincipalTable.PRINCIPAL.PK)
         .fetchOptional()
         .map(record -> (Integer)record.get(PrincipalTable.PRINCIPAL.PK))
         .orElseThrow(
            () -> new FailureException(String.format("Error fetching PK from inserted principal with ID %s", LogUtil.logSafeQuote(client.getId().asString())))
         );
   }

   public static void deletePrincipal(DSLContext db, TenantId tenantId, String principalId) {
      db.deleteFrom(PrincipalTable.PRINCIPAL).where(PrincipalTable.PRINCIPAL.ID.eq(principalId)).and(principalTenantMatches(tenantId)).execute();
   }

   public static Condition matchingTechClientId(Identity<TechnicalClient> techClientId) {
      return PrincipalTable.PRINCIPAL.ID.eq(techClientId.asString());
   }

   public static Condition principalTenantMatches(TenantId tenantId) {
      return PrincipalTable.PRINCIPAL.TENANT_ID.eq(tenantId.tenantId());
   }

   public static Condition apiKeyTenantMatches(TenantId tenantId) {
      return ApiKeyTable.API_KEY.TENANT_ID.eq(tenantId.tenantId());
   }

   public static Condition subscriptionTenantMatches(TenantId tenantId) {
      return SubscriptionTable.SUBSCRIPTION.TENANT_ID.eq(tenantId.tenantId());
   }

   private static InsertSetMoreStep<PrincipalRecord> startPrincipalInsert(
      Optional<Sequence<Integer>> principalSequence, TenantId tenantId, DSLContext db, TechnicalClient client
   ) {
      return principalSequence.<InsertSetMoreStep>map(s -> insertPrincipalWithSequence(db, (Sequence<Integer>)s, client))
         .orElse(insertPrincipalWithoutSequence(db, client))
         .set(PrincipalTable.PRINCIPAL.DISPLAY_NAME, client.getDisplayName())
         .set(PrincipalTable.PRINCIPAL.LOCKED, client.getLockInfo().map(lock -> 1).orElse(0))
         .set(PrincipalTable.PRINCIPAL.LOCK_REASON, client.getLockInfo().map(LockInfo::getLockReason).orElse(null))
         .set(PrincipalTable.PRINCIPAL.LOCK_DATE, client.getLockInfo().map(LockInfo::getLockDate).map(DbTimestamps::ofInstant).orElse(null))
         .set(PrincipalTable.PRINCIPAL.CREATION_DATE, DbTimestamps.now())
         .set(PrincipalTable.PRINCIPAL.TENANT_ID, tenantId.tenantId())
         .set(PrincipalTable.PRINCIPAL.DESCRIPTION, client.getDescription().orElse(null))
         .set(PrincipalTable.PRINCIPAL.LABEL, client.getLabel().orElse(null));
   }

   private static InsertSetMoreStep<PrincipalRecord> insertPrincipalWithoutSequence(DSLContext db, TechnicalClient client) {
      return db.insertInto(PrincipalTable.PRINCIPAL).set(PrincipalTable.PRINCIPAL.ID, client.getId().asString());
   }

   private static InsertSetMoreStep<PrincipalRecord> insertPrincipalWithSequence(DSLContext db, Sequence<Integer> seq, TechnicalClient client) {
      return db.insertInto(PrincipalTable.PRINCIPAL)
         .set(PrincipalTable.PRINCIPAL.PK, seq.nextval())
         .set(PrincipalTable.PRINCIPAL.ID, client.getId().asString());
   }

   public static int updatePrincipal(DSLContext db, TechnicalClient client, TenantId tenantId) {
      return db.update(PrincipalTable.PRINCIPAL)
         .set(PrincipalTable.PRINCIPAL.DISPLAY_NAME, client.getDisplayName())
         .set(PrincipalTable.PRINCIPAL.LOCKED, client.getLockInfo().map(lock -> 1).orElse(0))
         .set(PrincipalTable.PRINCIPAL.LOCK_REASON, client.getLockInfo().map(LockInfo::getLockReason).orElse(null))
         .set(PrincipalTable.PRINCIPAL.LOCK_DATE, client.getLockInfo().map(LockInfo::getLockDate).map(DbTimestamps::ofInstant).orElse(null))
         .set(PrincipalTable.PRINCIPAL.DESCRIPTION, client.getDescription().orElse(null))
         .set(PrincipalTable.PRINCIPAL.LABEL, client.getLabel().orElse(null))
         .where(PrincipalTable.PRINCIPAL.ID.eq(client.getId().asString()))
         .and(principalTenantMatches(tenantId))
         .execute();
   }

   public static Optional<LockInfo> recordToLockInfo(Record record) {
      return Optional.of((Integer)record.get(PrincipalTable.PRINCIPAL.LOCKED))
         .flatMap(
            locked -> locked == 0
               ? Optional.empty()
               : Optional.of(
                  new LockInfo(
                     (String)record.get(PrincipalTable.PRINCIPAL.LOCK_REASON),
                     DbTimestamps.toInstant((LocalDateTime)record.get(PrincipalTable.PRINCIPAL.LOCK_DATE))
                  )
               )
         );
   }

   private SharedPrincipalQueries() {
      throw new AssertionError();
   }
}
