package com.airlock.iam.saml2.infrastructure.service.sp;

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.misc.log.LogUtil;
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.opensso.jaxb.metadata.EndpointType;
import com.airlock.iam.opensso.jaxb.metadata.IDPSSODescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.SingleSignOnServiceElement;
import com.airlock.iam.saml2.application.configuration.sp.Saml2FlowSpConfig;
import com.airlock.iam.saml2.application.service.sp.Saml2SpInitiatedSsoService;
import com.airlock.iam.saml2.domain.model.Saml2Binding;
import com.airlock.iam.saml2.domain.model.Saml2MetaManager;
import com.airlock.iam.saml2.domain.model.idp.Saml2IdpEntityId;
import com.airlock.iam.saml2.domain.model.sp.Saml2AuthnRequestId;
import com.airlock.iam.saml2.domain.model.sp.Saml2SpEntityId;
import com.airlock.iam.saml2.domain.model.sp.Saml2SpInitiatedSsoId;
import com.airlock.iam.saml2.domain.model.sp.Saml2SpInitiatedSsoSessionAttribute;
import com.airlock.iam.saml2.domain.model.sp.Saml2SpMetaAlias;
import com.airlock.iam.saml2.domain.service.Saml2FlowSpConfigService;
import com.sun.identity.saml2.meta.SAML2MetaUtils;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.xml.bind.JAXBElement;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class Saml2SpInitiatedSsoServiceImpl implements Saml2SpInitiatedSsoService {
   private static final ELogger LOG = ELoggerFactory.getLogger(Saml2SpInitiatedSsoServiceImpl.class);
   private final Provider<Saml2SpFederationLibrary> library;
   private final Provider<UserSession> userSession;
   private final Provider<Saml2FlowSpConfigService> spConfigService;
   private final Provider<Saml2MetaManager> metaManager;

   @Inject
   public Saml2SpInitiatedSsoServiceImpl(
      Provider<Saml2SpFederationLibrary> library,
      Provider<UserSession> userSession,
      Provider<Saml2FlowSpConfigService> spConfigService,
      Provider<Saml2MetaManager> metaManager
   ) {
      this.library = library;
      this.userSession = userSession;
      this.spConfigService = spConfigService;
      this.metaManager = metaManager;
   }

   @Override
   public void createAuthnRequest(Saml2SpInitiatedSsoId ssoIdFromInitRequest) {
      Saml2SpInitiatedSsoSessionAttribute attribute = ((UserSession)this.userSession.get())
         .findUserSessionAttribute(Saml2SpInitiatedSsoSessionAttribute.class)
         .orElseThrow(
            () -> new ViolationException(
               "SSO-ID not present in user session. SP-initiated SSO must be started by the 'SAML 2.0 User Identifying Step' in an Authentication Flow."
            )
         );
      if (!attribute.ssoId().equals(ssoIdFromInitRequest)) {
         throw new ViolationException("SSO-ID from init request does not match SSO-ID from user session.");
      }

      Saml2FlowSpConfig flowSpConfig = ((Saml2FlowSpConfigService)this.spConfigService.get())
         .findFlowSpConfig(attribute.spEntityId())
         .orElseThrow(() -> new FailureException("SP entity ID %s not found in configuration".formatted(LogUtil.logSafeQuote(attribute.spEntityId()))));
      Saml2SpInitiatedSsoServiceImpl.SpIdpInfo info = this.getSpAndIdpSettingsForHostedSp(flowSpConfig);
      Optional<Saml2Binding> configuredRequestBinding = flowSpConfig.getAuthnRequestBinding().binding();
      String realmName = SAML2MetaUtils.getRealmByMetaAlias(info.spMetaAlias().value());
      List<Saml2Binding> availableBindings = this.getAllIdpBindings(info, realmName);
      if (availableBindings.isEmpty()) {
         throw new FailureException("No SingleSignOnService configured in standard metadata of IDP " + LogUtil.logSafeQuote(info.idpEntityId()));
      }

      Saml2Binding selectedBinding = configuredRequestBinding.<Saml2Binding>map(
            configuredBinding -> availableBindings.stream()
               .filter(availableBinding -> availableBinding.equals(configuredBinding))
               .findFirst()
               .orElseThrow(
                  () -> new FailureException(
                     "No SingleSignOnService for binding "
                        + LogUtil.logSafeQuote(configuredRequestBinding.get())
                        + " configured in standard metadata of IDP "
                        + LogUtil.logSafeQuote(info.idpEntityId())
                        + " (or wrong \"Authn Request Binding\" configured in the \"SAML 2.0 Flow SP\")."
                  )
               )
         )
         .orElse(availableBindings.getFirst());
      LOG.info("SP-initiated SSO: Initiating SP-SSO with IDP " + info.idpEntityId() + " using binding " + selectedBinding);
      Saml2AuthnRequestId requestId = ((Saml2SpFederationLibrary)this.library.get()).generateNewSpInitiatedSsoRequestId();
      ((UserSession)this.userSession.get()).addUserSessionAttribute(attribute.withAuthnRequestId(requestId));
      ((Saml2SpFederationLibrary)this.library.get()).initiateAuthnRequest(info.spEntityId(), info.idpEntityId(), realmName, requestId, selectedBinding);
      LOG.debug("SP-initiated SSO: sending AuthnRequest to IDP %s".formatted(LogUtil.logSafeQuote(info.idpEntityId())));
   }

   private Saml2SpInitiatedSsoServiceImpl.SpIdpInfo getSpAndIdpSettingsForHostedSp(Saml2FlowSpConfig spConfig) {
      Saml2SpEntityId spEntityId = Saml2SpEntityId.spEntityId(spConfig.getSpEntityConfig().getEntityId());
      if (!((Saml2MetaManager)this.metaManager.get()).getAllHostedServiceProviderEntities("/").contains(spEntityId)) {
         throw new FailureException("Hosted SP metadata not found for SP entity ID \"" + spEntityId + "\"");
      }

      Saml2SpMetaAlias spMetaAlias = this.getMetaAliasForSpEntityId(spEntityId);
      Saml2IdpEntityId idpEntityId = Saml2IdpEntityId.idpEntityId(spConfig.getRemoteIdpEntitySettings().getEntityId());
      if (((Saml2MetaManager)this.metaManager.get()).getIDPSSODescriptor("/", idpEntityId).isEmpty()) {
         throw new FailureException("IdP metadata not found for IdP entity ID \"" + idpEntityId + "\"");
      }

      if (!((Saml2MetaManager)this.metaManager.get()).isTrustedProvider("/", spEntityId, idpEntityId)) {
         throw new FailureException("Not a trusted relationship between SP entity \"" + spEntityId + " and IdP entity \"" + idpEntityId + "\"");
      }

      if (LOG.isDebugEnabled()) {
         LOG.debug("SP-IdP settings: SP entity ID = '" + spEntityId + "', meta alias = '" + spMetaAlias + "',  IdP entity ID = '" + idpEntityId + "'");
      }

      return new Saml2SpInitiatedSsoServiceImpl.SpIdpInfo(spEntityId, spMetaAlias, idpEntityId);
   }

   private List<Saml2Binding> getAllIdpBindings(Saml2SpInitiatedSsoServiceImpl.SpIdpInfo info, String realmName) {
      IDPSSODescriptorType idpsso = ((Saml2MetaManager)this.metaManager.get()).getIDPSSODescriptor(realmName, info.idpEntityId()).orElseThrow();
      List<SingleSignOnServiceElement> ssoServiceList = idpsso.getSingleSignOnService();
      return ssoServiceList.isEmpty()
         ? Collections.emptyList()
         : ssoServiceList.stream()
            .filter(Objects::nonNull)
            .<EndpointType>map(JAXBElement::getValue)
            .map(EndpointType::getBinding)
            .filter(Objects::nonNull)
            .map(Saml2Binding::forValue)
            .collect(Collectors.toList());
   }

   private Saml2SpMetaAlias getMetaAliasForSpEntityId(Saml2SpEntityId spEntityId) {
      List<Saml2SpMetaAlias> spMetaAliases = ((Saml2MetaManager)this.metaManager.get()).getAllHostedServiceProviderMetaAliases("/");
      if (spMetaAliases.isEmpty()) {
         throw new FailureException("No meta aliases found for hosted SPs");
      }

      for (Saml2SpMetaAlias alias : spMetaAliases) {
         Optional<Saml2SpEntityId> entityForAlias = ((Saml2MetaManager)this.metaManager.get()).getEntityByMetaAlias(alias);
         if (entityForAlias.map(entity -> entity.equals(spEntityId)).orElse(false)) {
            return alias;
         }
      }

      throw new FailureException("Meta alias not found for SP entity ID '" + spEntityId + "'");
   }

   private record SpIdpInfo(Saml2SpEntityId spEntityId, Saml2SpMetaAlias spMetaAlias, Saml2IdpEntityId idpEntityId) {
   }
}
