package com.airlock.iam.admin.application.service.certificate;

import com.airlock.iam.admin.domain.model.certificate.Certificate;
import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.exception.EntityNotUniqueException;
import com.airlock.iam.base.api.domain.model.store.exception.StoreFailureException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.common.domain.model.identity.DefaultStringIdentity;
import com.airlock.iam.core.misc.impl.token.CertificateToken;
import com.airlock.iam.core.misc.persistency.token.TokenDataProvider;
import com.airlock.iam.core.misc.persistency.token.TokenDataProvider.TokenFilter;
import com.airlock.iam.core.misc.token.KnownTokenTypes;
import com.airlock.iam.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CertificateRepository {
   private final TokenDataProvider tokenDataProvider;
   private final SigningLogger signingLogger;

   @Inject
   public CertificateRepository(TokenDataProvider tokenDataProvider, SigningLogger signingLogger) {
      this.tokenDataProvider = tokenDataProvider;
      this.signingLogger = signingLogger;
   }

   public List<Certificate> getAll(UserId userId) {
      return this.toCertificates(this.filterByUser(userId));
   }

   public Certificate getById(UserId userId, String tokenId) {
      return this.findById(userId, tokenId)
         .orElseThrow(() -> new EntityNotFoundException(String.format("No certificate with ID '%s' found for user '%s'", tokenId, userId)));
   }

   public Optional<Certificate> findById(UserId userId, String tokenId) {
      TokenFilter filter = this.filterByUser(userId).filterTokenId(tokenId);
      List<Certificate> certs = this.toCertificates(filter);
      if (certs.isEmpty()) {
         return Optional.empty();
      } else if (certs.size() > 1) {
         throw new EntityNotUniqueException(String.format("Certificate with ID '%s' for user '%s' is not unique", tokenId, userId));
      } else {
         return Optional.of(certs.getFirst());
      }
   }

   public void delete(UserId userId, String tokenId) {
      Certificate cert = this.getById(userId, tokenId);

      try {
         this.tokenDataProvider.removeToken(cert.getId().asString());
         this.signingLogger.log("Deleted certificate with ID '%s' of user '%s'", new Object[]{tokenId, userId});
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   public Certificate add(UserId userId, String addedBy, Certificate certificate) {
      CertificateToken token = this.newToken();
      token.getDelegate().assign(userId.getValue(), addedBy);
      return this.saveInternal(certificate, token);
   }

   public Certificate save(Certificate certificate) {
      CertificateToken token = this.getToken(certificate.getId().asString());
      return this.saveInternal(certificate, token);
   }

   private Certificate saveInternal(Certificate certificate, CertificateToken token) {
      token.setCertificate(certificate.getCertificate().orElse(null));
      this.save(token);
      return this.tokenToCertificate(token);
   }

   private Certificate tokenToCertificate(CertificateToken token) {
      return Certificate.builder()
         .id(DefaultStringIdentity.identity(token.getId(), Certificate.class))
         .serialNumber(token.getSerialId())
         .certificate(this.getCertificateOf(token).orElse(null))
         .importedAt(token.getGenerationDate())
         .subject(token.getSubjectDn())
         .subjectAlternativeNames(Collections.singletonList(token.getSubjectAlternativeName()))
         .issuer(token.getIssuerDn())
         .build();
   }

   private Optional<X509Certificate> getCertificateOf(CertificateToken token) {
      try {
         return Optional.ofNullable(token.getCertificate());
      } catch (CertificateException e) {
         return Optional.empty();
      }
   }

   private CertificateToken newToken() {
      try {
         return new CertificateToken(this.tokenDataProvider.createNewToken(KnownTokenTypes.CERTIFICATE.name()));
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   private CertificateToken getToken(String id) {
      try {
         return new CertificateToken(this.tokenDataProvider.getToken(id));
      } catch (NotFoundException e) {
         throw new EntityNotFoundException(e);
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   private void save(CertificateToken token) {
      try {
         this.tokenDataProvider.updateToken(token.getDelegate());
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   private List<Certificate> toCertificates(TokenFilter filter) {
      try {
         return this.tokenDataProvider
            .get(filter)
            .stream()
            .<CertificateToken>map(CertificateToken::new)
            .map(this::tokenToCertificate)
            .collect(Collectors.toList());
      } catch (NotFoundException e) {
         return Collections.emptyList();
      } catch (PersisterException e) {
         throw new StoreFailureException(this, e);
      }
   }

   private TokenFilter filterByUser(UserId userId) {
      return new TokenFilter().filterType(KnownTokenTypes.CERTIFICATE.getId()).filterSingleUser(userId.getValue());
   }
}
