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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSession;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.domain.model.Issuer;
import com.airlock.iam.oauth2.domain.model.client.OAuth2Client;
import com.airlock.iam.oauth2.domain.model.logoutaction.BackchannelLogout;
import com.airlock.iam.oauth2.domain.model.logoutaction.BackchannelLogoutSettings;
import com.airlock.iam.oauth2.domain.model.logoutaction.BackchannelLogoutSettingsProvider;
import com.airlock.iam.oauth2.domain.model.logoutaction.OidcBackchannelLogoutGenerator;
import com.airlock.iam.oauth2.domain.model.oidc.OidcUserAgentSessionId;
import com.airlock.iam.oauth2.domain.model.oidc.OidcUserAgentSessions;
import com.airlock.iam.oauth2.domain.model.token.IdToken;
import com.airlock.iam.oauth2.domain.model.token.IdTokenBuilder;
import com.airlock.iam.security.util.crypto.random.AlphanumericRandomStringGenerator;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public final class OidcBackchannelLogoutGeneratorImpl implements OidcBackchannelLogoutGenerator {
   private static final ELogger LOG = ELoggerFactory.getLogger(OidcBackchannelLogoutGeneratorImpl.class);
   private final Provider<BackchannelLogoutSettingsProvider> openIdProvider;
   private final Provider<UserSession> userSession;

   @Inject
   public OidcBackchannelLogoutGeneratorImpl(Provider<BackchannelLogoutSettingsProvider> openIdProvider, Provider<UserSession> userSession) {
      this.userSession = userSession;
      this.openIdProvider = openIdProvider;
   }

   @Override
   public List<BackchannelLogout> generateLogouts() {
      Optional<OidcUserAgentSessions> sessions = ((UserSession)this.userSession.get()).findUserSessionAttribute(OidcUserAgentSessions.class);
      if (sessions.isEmpty()) {
         return Collections.emptyList();
      }

      Set<OidcBackchannelLogoutGeneratorImpl.UniqueSession> uniqueSessions = sessions.get()
         .sessions()
         .stream()
         .map(OidcBackchannelLogoutGeneratorImpl.UniqueSession::new)
         .collect(Collectors.toSet());
      return uniqueSessions.stream().flatMap(session -> this.createLogout(session, sessions.get().userAgentSessionId()).stream()).toList();
   }

   private Optional<BackchannelLogout> createLogout(OidcBackchannelLogoutGeneratorImpl.UniqueSession session, OidcUserAgentSessionId oidcUserAgentSessionId) {
      OAuth2ClientId clientId = session.clientId();
      Optional<BackchannelLogoutSettings> rpSettings = ((BackchannelLogoutSettingsProvider)this.openIdProvider.get())
         .getBackchannelLogoutSettings(session.asId(), clientId);
      if (rpSettings.isEmpty()) {
         LOG.info(String.format("Back-channel logout is disabled for RP %s on OP %s.", LogUtil.quote(clientId), LogUtil.quote(session.asId())));
      }

      return rpSettings.<IdTokenBuilder>flatMap(settings -> this.createToken(settings.issuer(), session.sub(), clientId, oidcUserAgentSessionId))
         .flatMap(token -> sign(rpSettings.get(), token))
         .map(
            token -> BackchannelLogoutImpl.builder()
               .openIdProvider(session.asId())
               .relyingParty(clientId)
               .logoutToken(token)
               .backchannelLogoutUri(rpSettings.get().clientSettings().backchannelLogoutUri())
               .httpClientSupplier(rpSettings.get().clientSettings().closeableHttpClientSupplier())
               .tokenRepresentation(rpSettings.get().tokenRepresentation())
               .build()
         );
   }

   private Optional<IdTokenBuilder> createToken(Issuer issuer, String sub, OAuth2ClientId clientId, OidcUserAgentSessionId oidcUserAgentSessionId) {
      IdTokenBuilder.IdTokenBuilderBuilder builder = IdTokenBuilder.builder()
         .logger(LOG)
         .issuer(issuer.getValue())
         .subject(sub)
         .audience(clientId.id())
         .issueTime(new Date())
         .expirationTime(Date.from(Instant.now().plus(Duration.ofMinutes(5L))))
         .oidcUserAgentSessionId(oidcUserAgentSessionId.id())
         .jwtId(AlphanumericRandomStringGenerator.generateAlphanumericRandomString(27))
         .claim("events", Map.of("http://schemas.openid.net/event/backchannel-logout", Collections.emptyMap()));

      try {
         return Optional.of(builder.build());
      } catch (IdTokenBuilder.IdTokenException e) {
         LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Failed to create OIDC Backchannel-Logout Token", e);
         return Optional.empty();
      }
   }

   private static Optional<String> sign(BackchannelLogoutSettings rpSettings, IdTokenBuilder token) {
      Optional<OAuth2Client> oAuth2Client = rpSettings.clientLoader().get();
      if (oAuth2Client.isEmpty()) {
         LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Failed to load OIDC Client");
         return Optional.empty();
      }

      try {
         IdToken signedToken = rpSettings.signer().signed(token, oAuth2Client.get());
         return Optional.of(signedToken.tokenString());
      } catch (IdTokenBuilder.IdTokenException e) {
         LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Failed to sign OIDC Backchannel-Logout Token", e);
         return Optional.empty();
      }
   }

   private record UniqueSession(AuthorizationServerId asId, OAuth2ClientId clientId, String sub) {
      private UniqueSession(OidcUserAgentSessions.OidcUserAgentSession session) {
         this(session.asId(), session.clientId(), session.sub());
      }
   }
}
