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

import com.airlock.iam.base.misc.json.JsonCoding;
import com.airlock.iam.database.infrastructure.generated.schema.tables.Oauth2SessionAttributeTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.Oauth2SessionTable;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.Oauth2SessionAttributeRecord;
import com.airlock.iam.database.infrastructure.generated.schema.tables.records.Oauth2SessionRecord;
import com.airlock.iam.oauth2.domain.model.session.OAuth2CustomSessionAttribute;
import com.airlock.iam.oauth2.domain.model.session.OAuth2Session;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionClaims;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionId;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.jooq.Record;
import org.jooq.Result;

final class OAuth2SessionMaterializer {
   public static OAuth2SessionMaterializer oAuth2SessionMaterializer() {
      return new OAuth2SessionMaterializer();
   }

   Optional<OAuth2Session> materializeOAuth2Session(Result<Record> records) {
      if (records.isEmpty()) {
         return Optional.empty();
      }

      OAuth2SessionId sessionId = this.sessionIdFromRecord((Record)records.getFirst());
      Set<OAuth2CustomSessionAttribute> attributes = this.attributesFromRecords(records);
      OAuth2SessionClaims claims = this.getPersistedClaims(records);
      OAuth2Session session = this.oAuth2Session(sessionId, attributes, claims);
      return Optional.of(session);
   }

   private OAuth2SessionClaims getPersistedClaims(Result<Record> records) {
      return Arrays.stream(((Record)records.getFirst()).fields()).anyMatch(f -> f.equals(Oauth2SessionTable.OAUTH2_SESSION.CLAIMS))
         ? this.claimsFromRecord((Record)records.getFirst())
         : OAuth2SessionClaims.noClaims();
   }

   Set<OAuth2Session> materializeOAuth2Sessions(Result<Record> records) {
      return records.isEmpty()
         ? Collections.emptySet()
         : records.stream()
            .reduce(new HashMap(), this::sessionConstructor, OAuth2SessionMaterializer::unsupportedException)
            .values()
            .stream()
            .map(rawSession -> this.oAuth2Session(rawSession.id(), rawSession.attributes(), rawSession.claims()))
            .collect(Collectors.toSet());
   }

   private OAuth2SessionId sessionIdFromRecord(Record record) {
      Oauth2SessionRecord sessionRecord = (Oauth2SessionRecord)record.into(Oauth2SessionTable.OAUTH2_SESSION);
      return OAuth2SessionId.oAuth2SessionId(sessionRecord.getId());
   }

   private Set<OAuth2CustomSessionAttribute> attributesFromRecords(Collection<Record> records) {
      return records.stream().map(this::attributeFromRecord).flatMap(Optional::stream).collect(Collectors.toSet());
   }

   private Optional<OAuth2CustomSessionAttribute> attributeFromRecord(Record record) {
      Oauth2SessionAttributeRecord attributeRecord = (Oauth2SessionAttributeRecord)record.into(Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE);
      return attributeRecord.get(Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.NAME) == null
         ? Optional.empty()
         : Optional.of(
            OAuth2CustomSessionAttribute.builder()
               .name((String)attributeRecord.get(Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.NAME))
               .value((String)attributeRecord.get(Oauth2SessionAttributeTable.OAUTH2_SESSION_ATTRIBUTE.CONTENT))
               .build()
         );
   }

   private OAuth2SessionClaims claimsFromRecord(Record record) {
      Oauth2SessionRecord sessionRecord = (Oauth2SessionRecord)record.into(Oauth2SessionTable.OAUTH2_SESSION);
      return Optional.ofNullable(sessionRecord.getClaims())
         .map(databaseData -> JsonCoding.fromJsonTo(databaseData, OAuth2SessionClaims.class))
         .orElseGet(OAuth2SessionClaims::noClaims);
   }

   private Map<OAuth2SessionId, OAuth2SessionMaterializer.RawOAuth2Session> sessionConstructor(
      Map<OAuth2SessionId, OAuth2SessionMaterializer.RawOAuth2Session> sessions, Record record
   ) {
      OAuth2SessionId sessionId = this.sessionIdFromRecord(record);
      OAuth2SessionMaterializer.RawOAuth2Session session = sessions.get(sessionId);
      if (session == null) {
         OAuth2SessionClaims claims = this.claimsFromRecord(record);
         session = new OAuth2SessionMaterializer.RawOAuth2Session(sessionId, new HashSet<>(), claims);
         sessions.put(sessionId, session);
      }

      this.attributeFromRecord(record).ifPresent(session.attributes()::add);
      return sessions;
   }

   private static Map<OAuth2SessionId, OAuth2SessionMaterializer.RawOAuth2Session> unsupportedException(
      Map<OAuth2SessionId, OAuth2SessionMaterializer.RawOAuth2Session> first, Map<OAuth2SessionId, OAuth2SessionMaterializer.RawOAuth2Session> second
   ) {
      throw new UnsupportedOperationException("Parallel streams are not supported");
   }

   private OAuth2Session oAuth2Session(OAuth2SessionId oAuth2SessionId, Set<OAuth2CustomSessionAttribute> customAttributes, OAuth2SessionClaims claims) {
      return new OAuth2Session(oAuth2SessionId, claims, customAttributes);
   }

   @SuppressFBWarnings(justification = "generated code")
   private OAuth2SessionMaterializer() {
   }

   private record RawOAuth2Session(OAuth2SessionId id, Set<OAuth2CustomSessionAttribute> attributes, OAuth2SessionClaims claims) {
   }
}
