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

import com.airlock.iam.base.api.domain.model.event.EventId;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.infrastructure.db.DbInstantConverter;
import com.airlock.iam.base.misc.exception.Exceptions;
import com.airlock.iam.common.api.domain.model.event.EventData;
import com.airlock.iam.common.domain.service.event.EventOutboxRepository;
import com.airlock.iam.common.infrastructure.jackson.IamJsonSerialiser;
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 com.google.common.annotations.VisibleForTesting;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.SelectFieldOrAsterisk;

public final class EventOutboxRepositoryImpl implements EventOutboxRepository {
   private final Supplier<DSLContext> jooqDslProvider;
   private final TenantId tenantId;

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

   @Override
   public void insert(EventOutboxRepository.EventOutboxInsertData insertData) {
      Exceptions.wrapException(() -> this.insertEventsInternal(insertData), e -> new FailureException("Failed to insert event for " + insertData, e));
   }

   @VisibleForTesting
   Optional<EventData> findByEventId(EventId eventId) {
      return Exceptions.wrapException(
         () -> this.db()
            .select(new SelectFieldOrAsterisk[0])
            .from(Tables.EVENT_OUTBOX)
            .where(Tables.EVENT_OUTBOX.EVENT_ID.eq(eventId.value()))
            .and(Tables.EVENT_OUTBOX.TENANT_ID.eq(this.tenantId.tenantId()))
            .fetchOptional(this::toEventData),
         e -> new FailureException("Failed to find event with ID " + eventId, e)
      );
   }

   @Override
   public void deleteByEventIds(Collection<EventId> eventIds) {
      if (!eventIds.isEmpty()) {
         Exceptions.wrapException(
            () -> JooqTransaction.jooqTransaction(this.db()).execute(db -> this.deleteEvents(db, eventIds)),
            e -> new FailureException("Failed to delete event with IDs " + eventIds, e)
         );
      }
   }

   @Override
   public List<EventData> getNext(int limit) {
      ViolationException.checkTrue(limit > 0, "event limit must be positive");
      return Exceptions.wrapException(
         () -> this.db()
            .select(new SelectFieldOrAsterisk[0])
            .from(Tables.EVENT_OUTBOX)
            .where(Tables.EVENT_OUTBOX.TENANT_ID.eq(this.tenantId.tenantId()))
            .orderBy(Tables.EVENT_OUTBOX.CREATED_AT.asc(), Tables.EVENT_OUTBOX.TIE_BREAKER.asc())
            .limit(limit)
            .fetch(this::toEventData),
         e -> new FailureException("Failed to fetch next " + limit + " events from outbox", e)
      );
   }

   @Override
   public int count() {
      return Exceptions.wrapException(
         () -> this.db().fetchCount(Tables.EVENT_OUTBOX, Tables.EVENT_OUTBOX.TENANT_ID.eq(this.tenantId.tenantId())),
         e -> new FailureException("Failed to count events in outbox", e)
      );
   }

   private void deleteEvents(DSLContext db, Collection<EventId> eventIds) {
      List<String> ids = eventIds.stream().map(EventId::value).toList();
      db.deleteFrom(Tables.EVENT_OUTBOX).where(Tables.EVENT_OUTBOX.EVENT_ID.in(ids)).and(Tables.EVENT_OUTBOX.TENANT_ID.eq(this.tenantId.tenantId())).execute();
   }

   private EventData toEventData(Record record) {
      return EventData.builder()
         .eventId(EventId.eventId((String)record.get(Tables.EVENT_OUTBOX.EVENT_ID)))
         .eventType(() -> (String)record.get(Tables.EVENT_OUTBOX.EVENT_TYPE))
         .payload(new EventData.EventPayload(IamJsonSerialiser.fromJson((String)record.get(Tables.EVENT_OUTBOX.PAYLOAD))))
         .createdAt(DbInstantConverter.toInstant((Long)record.get(Tables.EVENT_OUTBOX.CREATED_AT)))
         .build();
   }

   private void insertEventsInternal(EventOutboxRepository.EventOutboxInsertData data) {
      this.db()
         .insertInto(Tables.EVENT_OUTBOX)
         .set(Tables.EVENT_OUTBOX.EVENT_ID, data.eventId().value())
         .set(Tables.EVENT_OUTBOX.EVENT_TYPE, data.eventType().name())
         .set(Tables.EVENT_OUTBOX.PAYLOAD, IamJsonSerialiser.toJson(data.payload().value()))
         .set(Tables.EVENT_OUTBOX.TIE_BREAKER, data.tieBreaker())
         .set(Tables.EVENT_OUTBOX.TENANT_ID, this.tenantId.tenantId())
         .set(Tables.EVENT_OUTBOX.CREATED_AT, DbInstantConverter.toNanoSecondsSinceEpoch(Instant.now()))
         .execute();
   }

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