package study.oauthlab;

import java.time.Duration;
import java.util.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.resource.introspection.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;

@Configuration @Profile("rs") @EnableMethodSecurity
class ResourceServer {
    @Bean JwtDecoder decoder(@Value("${lab.issuer}") String issuer) {
        var decoder=NimbusJwtDecoder.withJwkSetUri(issuer+"/oauth2/jwks").jwsAlgorithm(SignatureAlgorithm.RS256).build();
        var clock=new JwtTimestampValidator(Duration.ZERO);
        clock.setAllowEmptyExpiryClaim(false);
        decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(issuer), clock,
            new JwtAudienceValidator("orders-api"),
            jwt -> jwt.getClaimAsString("tenant") != null && Set.of("t1","t2").contains(jwt.getClaimAsString("tenant"))
                ? OAuth2TokenValidatorResult.success() : OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token","trusted tenant required",null))));
        return decoder;
    }
    @Bean OpaqueTokenIntrospector introspector(@Value("${lab.issuer}") String issuer) {
        var remote=SpringOpaqueTokenIntrospector.withIntrospectionUri(issuer+"/oauth2/introspect")
            .clientId("lab-client").clientSecret("lab-secret").build();
        return token -> {
            var principal=remote.introspect(token);
            Object aud=principal.getAttribute("aud");
            if(!(aud instanceof Collection<?> values) || !values.contains("orders-api"))
                throw new BadOpaqueTokenException("wrong audience");
            return principal;
        };
    }
    @Bean @Order(1) SecurityFilterChain jwt(HttpSecurity http) throws Exception {
        http.securityMatcher("/jwt/**").sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // This API accepts only explicit Authorization Bearer headers; no browser automatic credentials.
            .csrf(c -> c.disable()).authorizeHttpRequests(a -> a.requestMatchers("/jwt/admin").hasAuthority("SCOPE_admin")
                .requestMatchers("/jwt/read").hasAuthority("SCOPE_orders.read").anyRequest().authenticated())
            .oauth2ResourceServer(o -> o.jwt(j -> {}).withObjectPostProcessor(failureResponses()));
        return http.build();
    }
    @Bean @Order(2) SecurityFilterChain opaque(HttpSecurity http) throws Exception {
        http.securityMatcher("/opaque/**").sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(c -> c.disable()).authorizeHttpRequests(a -> a.anyRequest().hasAuthority("SCOPE_orders.read"))
            .oauth2ResourceServer(o -> o.opaqueToken(t -> {}).withObjectPostProcessor(failureResponses()));
        return http.build();
    }
    static ObjectPostProcessor<BearerTokenAuthenticationFilter> failureResponses() {
        return new ObjectPostProcessor<BearerTokenAuthenticationFilter>() {
                public <O extends BearerTokenAuthenticationFilter> O postProcess(O filter) {
                    filter.setAuthenticationFailureHandler((request,response,error) -> {
                        if (error instanceof AuthenticationServiceException) {
                            response.setStatus(503);
                            response.setContentType("application/json");
                            response.getWriter().write("{\"error\":\"identity_provider_unavailable\"}");
                        } else new BearerTokenAuthenticationEntryPoint().commence(request,response,error);
                    });
                    return filter;
                }
            };
    }
    @Bean @Order(3) SecurityFilterChain deny(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(a -> a.anyRequest().denyAll()); return http.build();
    }
    @Bean Ownership ownership() { return new Ownership(); }
    public static class Ownership {
        // Synthetic server-side records; tenant/user come from validated authentication, never caller authority.
        final Map<String,List<String>> records=Map.of("own",List.of("t1","alice"),"other",List.of("t1","bob"),"foreign",List.of("t2","alice"));
        public boolean allowed(Authentication auth,String id,String tenant) {
            if(!(auth instanceof org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken jwt)) return false;
            String trustedTenant=jwt.getToken().getClaimAsString("tenant");
            return Objects.equals(tenant,trustedTenant) && Objects.equals(records.get(id),List.of(trustedTenant,auth.getName()));
        }
    }
    @Service @Profile("rs") public static class Orders {
        @PreAuthorize("hasAuthority('SCOPE_orders.write')")
        public Map<String,String> write() { return Map.of("result","authorized-write"); }
        @PreAuthorize("hasAuthority('SCOPE_orders.read') and @ownership.allowed(authentication,#id,#tenant)")
        public Map<String,String> read(String id,String tenant) { return Map.of("id",id,"tenant",tenant,"result","owned"); }
    }
    @RestController @Profile("rs") static class Endpoints {
        final Orders orders;
        Endpoints(Orders orders) {this.orders=orders;}
        @GetMapping({"/jwt/read","/opaque/read","/jwt/admin"}) Map<String,Object> read(Authentication a) {
            return Map.of("name",a.getName(),"authorities",a.getAuthorities().stream().map(Object::toString).toList());
        }
        @GetMapping("/jwt/method") Map<String,String> method() {return orders.write();}
        @GetMapping("/jwt/orders/{id}") Map<String,String> order(@PathVariable String id,@RequestParam String tenant) { return orders.read(id,tenant); }
    }
}
