package muni.fi.cz.jobportal.configuration.security;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import lombok.RequiredArgsConstructor;
import muni.fi.cz.jobportal.configuration.properties.JobPortalApplicationProperties;
import muni.fi.cz.jobportal.utils.StaticObjectFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;

import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPublicKey;
import java.util.List;

/**
 * Configuration class for security settings of the application
 *
 * @author Vitalii Bortsov
 */
@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true
)
@Import(SecurityProblemSupport.class)
@RequiredArgsConstructor
public class SecurityConfiguration {

  private static final String[] SWAGGER_PUBLIC_ENDPOINTS = {
    "/swagger-ui.html",
    "/swagger-ui/**",
    "/v3/api-docs/**",
  };

  private static final String UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
  private static final List<String> ALLOWED_CORS_HEADERS = List.of("x-requested-with", "authorization", "origin",
    "content-type", "version",
    "content-disposition", "location");
  private final JobPortalApplicationProperties applicationProperties;
  private final SecurityProblemSupport securityProblemSupport;

  @Bean
  public CorsConfigurationSource corsFilter() {
    final var cfg = new CorsConfiguration();
    cfg.addAllowedOrigin("*");
    cfg.addAllowedHeader("*");
    cfg.addAllowedMethod("*");
    cfg.setExposedHeaders(ALLOWED_CORS_HEADERS);

    final var source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", cfg);

    return source;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.headers().cacheControl().disable();

    http.cors().configurationSource(corsFilter());
    http
      .authorizeHttpRequests()
      .antMatchers(HttpMethod.POST, "/auth/**").permitAll()
      .antMatchers(HttpMethod.GET, "/auth/**", "/companies/**", "/job-categories/**", "/positions", "/positions/")
      .permitAll()
      .regexMatchers(HttpMethod.GET, "/positions/" + UUID_REGEX, "/users/" + UUID_REGEX + "/avatar").permitAll()
      .antMatchers(HttpMethod.GET, SWAGGER_PUBLIC_ENDPOINTS).permitAll()
      .anyRequest()
      .authenticated();

    http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);

    http.httpBasic(Customizer.withDefaults());

    http.exceptionHandling()
      .authenticationEntryPoint(securityProblemSupport)
      .accessDeniedHandler(securityProblemSupport);

    return http.build();
  }

  @Bean
  public RSAKey rsaKey() {
    final var storeProperties = applicationProperties.getKeyStore();
    try (final var is = storeProperties.getLocation().getInputStream()) {
      final var ks = KeyStore.getInstance(KeyStore.getDefaultType());
      ks.load(is, storeProperties.getKeyStorePassword().toCharArray());

      final var instance = storeProperties.getKeyPairName();
      final var key = ks.getKey(instance, storeProperties.getKeyPairPassword().toCharArray());
      if (key instanceof PrivateKey pk) {
        final var cert = ks.getCertificate(instance);
        final var publicKey = cert.getPublicKey();
        return new RSAKey.Builder((RSAPublicKey) publicKey)
          .privateKey(pk)
          .keyID(storeProperties.getKid())
          .build();
      } else {
        throw new BeanInitializationException("Failed to initialize KeyPair provider. Create key is not private");
      }
    } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException |
             CertificateException ex) {
      throw new BeanInitializationException(ex.getMessage(), ex);
    }
  }

  @Bean
  public JwtDecoder jwtDecoder(StaticObjectFactory staticObjectFactory) {
    try {
      final var result = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey().toPublicKey()).build();
      final var validator = new JwtTimestampValidator();
      validator.setClock(staticObjectFactory.getClock());

      result.setJwtValidator(validator);

      return result;
    } catch (JOSEException ex) {
      throw new BeanInitializationException(ex.getMessage(), ex);
    }
  }

  @Bean
  public JwtEncoder jwtEncoder() {
    final var source = new ImmutableJWKSet<>(new JWKSet(rsaKey()));
    return new NimbusJwtEncoder(source);
  }

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
    throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}
