package com.oguerreiro.resilient.security.saml2;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.stream.Collectors;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;

import com.oguerreiro.resilient.security.saml2.ResilientSaml2Properties.Registration;
import com.oguerreiro.resilient.security.saml2.ResilientSaml2Properties.UrlConfig;

@Configuration
public class Saml2RelyingPartyRegistrationRepository {

  @Bean
  @ConditionalOnProperty(name = "resilient.security.saml2.enabled", havingValue = "true")
  public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(
      Saml2CredentialLoader saml2CredentialLoader, ResilientSaml2Properties resilientSaml2Properties) throws Exception {

    if (!resilientSaml2Properties.isEnabled())
      return null;

    // No registration ?
    if (resilientSaml2Properties.getRelyingparty().getRegistration().size() == 0)
      return null;

    // No IDP-ID ?
    if (resilientSaml2Properties.getIdpId() == null || resilientSaml2Properties.getIdpId().isBlank()) {
      return null;
    }

    // Get the configured IDP
    Registration registration = resilientSaml2Properties.getRelyingparty().getRegistration().get(
        resilientSaml2Properties.getIdpId());
    // The intended IDP wasn't found
    if (registration == null)
      return null;

    // Load credentials
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.assertingparty.verification.credentials.certificate-location: "classpath:saml/idp-public.cert"
    String idpCertificateConf = registration.getAssertingparty().getVerification().getCredentials().get(
        0).getCertificateLocation();
    X509Certificate idpCertificate = saml2CredentialLoader.loadCertificate(idpCertificateConf);
    Saml2X509Credential idpVerificationCredential = new Saml2X509Credential(idpCertificate,
        Saml2X509Credential.Saml2X509CredentialType.VERIFICATION);

    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.signing.credentials.certificate-location: "classpath:saml/public.cert"
    String spCertificateConf = registration.getSigning().getCredentials().get(0).getCertificateLocation();
    X509Certificate spCertificate = saml2CredentialLoader.loadCertificate(spCertificateConf);
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.signing.credentials.private-key-location: "classpath:saml/private.key"
    String spPrivateKeyConf = registration.getSigning().getCredentials().get(0).getPrivateKeyLocation();
    PrivateKey spPrivateKey = saml2CredentialLoader.loadPrivateKey(spPrivateKeyConf);
    Saml2X509Credential spSigningCredential = new Saml2X509Credential(spPrivateKey, spCertificate,
        Saml2X509Credential.Saml2X509CredentialType.SIGNING);

    // Build RelyingPartyRegistration
    //@formatter:off
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>, the name of the registration, in this case, mock-idp
    String registrationIdConf = resilientSaml2Properties.getIdpId();
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.assertingparty.entity-id: "http://localhost:3000/saml/metadata"
    String entityIdConf = registration.getAssertingparty().getEntityId();
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.assertingparty.single-sign-on.url: "http://localhost:3000/saml/sso"
    String singleSignOnServiceLocationConf = this.buildUrlServiceLocation(registration.getAssertingparty().getSingleSignOn());
    // YML : resilient.security.saml2.relyingparty.registration.<mock-idp>.assertingparty.single-logout.url: "http://localhost:3000/saml/slo"
    String singleLogoutServiceLocationConf = this.buildUrlServiceLocation(registration.getAssertingparty().getSingleLogout());
    // Build from configuration. Results in something like : "https://localhost:8443/saml2/service-provider-metadata/mock-idp"
    String serviceProviderEntityIdConf = resilientSaml2Properties.getBaseUrl() + "/saml2/service-provider-metadata/" + registrationIdConf;
    // Build from configuration. Results in something like : "https://localhost:8443/login/saml2/sso/mock-idp"
    String assertionConsumerServiceLocationConf = resilientSaml2Properties.getBaseUrl() + "/login/saml2/sso/" + registrationIdConf;
    RelyingPartyRegistration rpRegistration = RelyingPartyRegistration
        .withRegistrationId(registrationIdConf)
        .entityId(serviceProviderEntityIdConf)
        .assertionConsumerServiceLocation(assertionConsumerServiceLocationConf)
        .signingX509Credentials(c -> c.add(spSigningCredential))
        .assertingPartyDetails(party -> party
            .entityId(entityIdConf)
            .singleSignOnServiceLocation(singleSignOnServiceLocationConf)
            .singleLogoutServiceLocation(singleLogoutServiceLocationConf)
            .wantAuthnRequestsSigned(false)
            .verificationX509Credentials(c -> c.add(idpVerificationCredential))
        )
        .build();
    //@formatter:on

    return new InMemoryRelyingPartyRegistrationRepository(rpRegistration);
  }

  private String buildUrlServiceLocation(UrlConfig urlConfig) {
    StringBuilder urlServiceLocation = new StringBuilder();
    urlServiceLocation.append(urlConfig.getUrl());

    String queryString = "";
    if (urlConfig.getQueryParameters() != null && !urlConfig.getQueryParameters().isEmpty()) {
      //@formatter:off
      queryString = urlConfig.getQueryParameters().entrySet().stream()
          .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
          .collect(Collectors.joining("&", "?", ""));
      //@formatter:on      

      urlServiceLocation.append(queryString);
    }

    return urlServiceLocation.toString();
  }

  private static String encode(String value) {
    return URLEncoder.encode(value, StandardCharsets.UTF_8);
  }
}
