package de.avatic.lcc.config;

import de.avatic.lcc.config.filter.AuthorityRefreshFilter;
import de.avatic.lcc.config.filter.DevUserEmulationFilter;
import de.avatic.lcc.config.filter.SelfIssuedJwtFilter;
import de.avatic.lcc.model.db.users.User;
import de.avatic.lcc.repositories.users.GroupRepository;
import de.avatic.lcc.repositories.users.UserRepository;
import de.avatic.lcc.service.apps.JwtTokenService;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.*;
import java.util.function.Supplier;


@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
    @Value("${lcc.allowed_cors}")
    private String allowedCors;

    @Value("${lcc.allowed_oauth_token_cors:*}") // Default: alle Origins
    private String oauthTokenCors;

    @Value("${lcc.auth.identify.by}")
    private String identifyBy;

    @Value("${lcc.auth.claim.workday}")
    private String workdayClaim;

    @Value("${lcc.auth.claim.email}")
    private String emailClaim;

    @Value("${lcc.auth.claim.firstname}")
    private String firstnameClaim;

    @Value("${lcc.auth.claim.lastname}")
    private String lastNameClaim;

    @Value("${lcc.auth.claim.ignore.workday}")
    private boolean ignoreWorkdayClaim;


    @Bean
    @Profile("!dev & !test")  // Only active when NOT in dev profile
    public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService, AuthorityRefreshFilter authorityRefreshFilter) throws Exception {
        http
                .cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                        .requestMatchers("/actuator/health").permitAll()
                        .requestMatchers("/actuator/**").hasRole("SERVICE")
                        .requestMatchers("/oauth2/token").permitAll()
                        .requestMatchers("/api/**").authenticated()
                        .requestMatchers("/api/dev/**").denyAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2

                        .defaultSuccessUrl("/", true)
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .bearerTokenResolver(bearerTokenResolver(jwtTokenService))
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(jwtAuthenticationConverter())
                        )
                )
                .exceptionHandling(ex -> ex
                        .defaultAuthenticationEntryPointFor(
                                new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
                                new AntPathRequestMatcher("/api/**")
                        )
                )
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers("/oauth2/token")
                        .ignoringRequestMatchers("/login/oauth2/code/**")
                        .requireCsrfProtectionMatcher(request -> {
                            String requestUri = request.getRequestURI();
                            if (requestUri.startsWith("/oauth2/") || requestUri.startsWith("/login/oauth2/")) {
                                return false;
                            }

                            String authHeader = request.getHeader("Authorization");
                            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                                return false;
                            }

                            return !"GET".equalsIgnoreCase(request.getMethod());
                        })
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
                )
                .addFilterAfter(authorityRefreshFilter, BearerTokenAuthenticationFilter.class)
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .addFilterBefore(
                        new SelfIssuedJwtFilter(jwtTokenService),
                        BearerTokenAuthenticationFilter.class
                );

        return http.build();
    }

    @Bean
    @Profile("!dev & !test")
    public CorsConfigurationSource prodCorsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration tokenConfiguration = new CorsConfiguration();
        if ("*".equals(oauthTokenCors)) {
            tokenConfiguration.setAllowedOriginPatterns(List.of("*"));
        } else {
            String[] tokenOrigins = oauthTokenCors.split(",");
            for (int i = 0; i < tokenOrigins.length; i++) {
                tokenOrigins[i] = tokenOrigins[i].trim();
            }
            if (tokenOrigins.length != 0) {
                tokenConfiguration.setAllowedOrigins(Arrays.asList(tokenOrigins));
            }
        }
        tokenConfiguration.setAllowedMethods(Arrays.asList("POST", "OPTIONS"));
        tokenConfiguration.setAllowedHeaders(List.of("*"));
        tokenConfiguration.setAllowCredentials(true);
        tokenConfiguration.setMaxAge(3600L);


        source.registerCorsConfiguration("/oauth2/token", tokenConfiguration);


        CorsConfiguration configuration = new CorsConfiguration();
        if ("*".equals(allowedCors)) {
            configuration.setAllowedOriginPatterns(List.of("*"));
        } else {
            String[] origins = allowedCors.split(",");
            for (int i = 0; i < origins.length; i++) {
                origins[i] = origins[i].trim();
            }
            if (origins.length != 0) {
                configuration.setAllowedOrigins(Arrays.asList(origins));
            }
        }
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page"));

        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Bean
    @Profile("dev | test")
    public SecurityFilterChain devSecurityFilterChain(HttpSecurity http, UserRepository userRepository, JwtTokenService jwtTokenService) throws Exception {
        return http
                .cors(cors -> cors.configurationSource(devCorsConfigurationSource())) // Dev CORS
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                        .requestMatchers("/actuator/health").permitAll()
                        .requestMatchers("/actuator/**").permitAll()
                        .requestMatchers("/oauth2/token").permitAll()
                        .requestMatchers("/api/**").permitAll()
                        .requestMatchers("/api/dev/**").permitAll()
                        .anyRequest().permitAll())
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers("/oauth2/token") // CSRF für OAuth deaktivieren
                        .ignoringRequestMatchers("/api/**") // Optional: für alle API-Endpunkte
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
                )
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .addFilterBefore(new DevUserEmulationFilter(userRepository), BasicAuthenticationFilter.class)
                .addFilterBefore(
                        new SelfIssuedJwtFilter(jwtTokenService),
                        BearerTokenAuthenticationFilter.class
                )
                .build();
    }

    @Bean
    @Profile("dev | test")
    public CorsConfigurationSource devCorsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(List.of("http://localhost:*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

        configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page"));


        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        // Für Entra ID Tokens
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthoritiesClaimName("scp");
        converter.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
        return jwtConverter;
    }

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


    @Bean
    @Profile("!dev & !test")
    public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(UserRepository userRepository, GroupRepository groupRepository) {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            try {
                log.info("=== OIDC User Service called ===");

                OidcUser oidcUser = delegate.loadUser(userRequest);
                log.info("OIDC User loaded successfully");

                Integer userId = null;

                // Debug: Print all claims
                log.debug("=== ID Token Claims ===");
                oidcUser.getIdToken().getClaims().forEach((key, value) ->
                        log.debug("{}: {}", key, value)
                );
                log.debug("======================");

                Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities());

                User user = null;

                String workdayId = oidcUser.getAttribute(workdayClaim);
                String email = oidcUser.getAttribute(emailClaim);

                String firstName = oidcUser.getAttribute(firstnameClaim);
                String lastName = oidcUser.getAttribute(lastNameClaim);

                log.info("Claims extracted - email: {}, workdayId: {}, firstName: {}, lastName: {}",
                        email, workdayId, firstName, lastName);

                if (identifyBy.equals("email") && email != null && !email.isEmpty()) {
                    log.debug("Fetch user by email {}", email);
                    user = userRepository.getByEmail(email);
                } else if (identifyBy.equals("workday") && workdayId != null && !workdayId.isEmpty()) {
                    log.debug("Fetch user by workday id {}", workdayId);
                    user = userRepository.getByWorkdayId(workdayId).orElse(null);
                }

                if (user != null) {
                    userId = user.getId();
                    log.info("User found with ID: {}", userId);
                } else {
                    if (email != null && firstName != null && lastName != null && (ignoreWorkdayClaim || workdayId != null)) {
                        log.info("Creating new user");
                        var isFirstUser = userRepository.count() == 0;
                        user = LccOidcUser.createDatabaseUser(email, firstName, lastName, ignoreWorkdayClaim ? generateRandomWorkdayId() : workdayId, isFirstUser);
                        userId = userRepository.update(user);
                        log.info("New user created with ID: {}", userId);
                    } else {
                        log.warn("Unable to create user - email: {}, firstName: {}, lastName: {}, workdayId: {}",
                                email, firstName, lastName, workdayId);
                        mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_NONE"));
                    }
                }

                if (user != null) {
                    user.getGroups().forEach(g -> log.debug("Local group: {}", g.getName()));
                    user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase())));
                }

                log.info("=== OIDC User Service completed successfully ===");

                return new LccOidcUser(
                        mappedAuthorities,
                        oidcUser.getIdToken(),
                        oidcUser.getUserInfo(),
                        "preferred_username",
                        userId
                );

            } catch (Exception e) {
                log.error("=== FATAL ERROR in oidcUserService ===", e);
                log.error("Exception type: {}", e.getClass().getName());
                log.error("Exception message: {}", e.getMessage());
                log.error("Stack trace:", e);
                throw e; // Re-throw to maintain Spring Security behavior
            }
        };
    }

    private String generateRandomWorkdayId() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    @Bean
    @Profile("!dev & !test")
    public BearerTokenResolver bearerTokenResolver(JwtTokenService jwtTokenService) {
        return request -> {
            String authHeader = request.getHeader("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String token = authHeader.substring(7);

                try {
                    Claims claims = jwtTokenService.parseClaimsWithoutValidation(token);
                    String tokenType = claims.get("token_type", String.class);
                    if ("ext_app".equals(tokenType)) {
                        return null;
                    }
                } catch (Exception e) {
                    log.debug("Failed to parse token without validation", e);
                }

                return token;
            }
            return null;
        };
    }

    public static final class LccCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
        private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler();

        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response,
                           Supplier<CsrfToken> csrfToken) {
            this.delegate.handle(request, response, csrfToken);
        }

        @Override
        public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
            if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
                return super.resolveCsrfTokenValue(request, csrfToken);
            }
            return this.delegate.resolveCsrfTokenValue(request, csrfToken);
        }
    }

    public static final class CsrfCookieFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response,
                                        @NotNull FilterChain filterChain) throws ServletException, IOException {
            // Skip CSRF cookie for token endpoint
            if (!request.getRequestURI().startsWith("/oauth2/token")) {
                CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
                if (csrfToken != null) {
                    csrfToken.getToken();
                }
            }
            filterChain.doFilter(request, response);
        }
    }
}
