package com.cameleer.server.app.security;

import com.cameleer.server.app.metrics.ServerMetrics;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.security.JwtService;
import com.cameleer.server.core.security.UserRepository;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.List;

/**
 * Spring Security configuration for JWT-based stateless authentication with RBAC.
 * <p>
 * Public endpoints: health, agent registration, refresh, auth, API docs, Swagger UI, static resources.
 * All other endpoints require a valid JWT access token with appropriate roles.
 * <p>
 * When {@code security.oidc-issuer-uri} is configured, builds an OIDC {@link JwtDecoder}
 * for validating external access tokens (Logto M2M / OIDC user tokens) as a fallback
 * after internal HMAC validation.
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                            JwtService jwtService,
                                            AgentRegistryService registryService,
                                            SecurityProperties securityProperties,
                                            CorsConfigurationSource corsConfigurationSource,
                                            UserRepository userRepository,
                                            ServerMetrics serverMetrics) throws Exception {
        JwtDecoder oidcDecoder = null;
        String issuer = securityProperties.getOidc().getIssuerUri();
        if (issuer != null && !issuer.isBlank()) {
            try {
                oidcDecoder = buildOidcDecoder(securityProperties);
                log.info("OIDC resource server enabled: issuer={}", issuer);
            } catch (Exception e) {
                log.error("Failed to initialize OIDC decoder for issuer={}: {}", issuer, e.getMessage());
            }
        }

        http
                .cors(cors -> cors.configurationSource(corsConfigurationSource))
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        // Public endpoints
                        .requestMatchers(
                                "/api/v1/health",
                                "/api/v1/prometheus",
                                "/api/v1/branding/**",
                                "/api/v1/agents/register",
                                "/api/v1/agents/*/refresh",
                                "/api/v1/auth/**",
                                "/api/v1/api-docs/**",
                                "/api/v1/swagger-ui/**",
                                "/swagger-ui/**",
                                "/v3/api-docs/**",
                                "/swagger-ui.html",
                                "/error",
                                "/",
                                "/index.html",
                                "/config.js",
                                "/favicon.svg",
                                "/assets/**"
                        ).permitAll()

                        // Agent-only endpoints
                        .requestMatchers("/api/v1/data/**").hasRole("AGENT")
                        .requestMatchers("/api/v1/agents/*/heartbeat").hasRole("AGENT")
                        .requestMatchers("/api/v1/agents/*/events").hasRole("AGENT")
                        .requestMatchers("/api/v1/agents/*/commands/*/ack").hasRole("AGENT")

                        // Command endpoints — operator+ only
                        .requestMatchers(HttpMethod.POST, "/api/v1/agents/*/commands").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST, "/api/v1/agents/groups/*/commands").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST, "/api/v1/agents/commands").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST, "/api/v1/agents/*/replay").hasAnyRole("OPERATOR", "ADMIN")

                        // Search endpoints
                        .requestMatchers(HttpMethod.GET, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
                        .requestMatchers(HttpMethod.POST, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")

                        // Application config endpoints (legacy flat shape — removed as controllers migrate to /environments/{env}/...)
                        .requestMatchers(HttpMethod.GET, "/api/v1/config/*").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
                        .requestMatchers(HttpMethod.PUT, "/api/v1/config/*").hasAnyRole("OPERATOR", "ADMIN")

                        // Agent-authoritative config (post-migration split from /api/v1/config/{app})
                        .requestMatchers(HttpMethod.GET, "/api/v1/agents/config").hasRole("AGENT")

                        // Env-scoped config & settings (specific rules BEFORE the generic /apps/** OPERATOR rule)
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/config").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/apps/*/config").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/apps/*/processor-routes").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.PUT, "/api/v1/environments/*/apps/*/config").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/apps/*/config/test-expression").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers("/api/v1/environments/*/app-settings").hasRole("ADMIN")
                        .requestMatchers("/api/v1/environments/*/apps/*/settings").hasRole("ADMIN")

                        // Env-scoped data reads (executions/stats/logs/routes/agents/diagrams)
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/stats/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/errors/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/attributes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/logs").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/routes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/agents/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/environments/*/apps/*/routes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")

                        // Env-scoped app & deployment management (OPERATOR+) — catch-all for /environments/*/apps/**
                        .requestMatchers("/api/v1/environments/*/apps/**").hasAnyRole("OPERATOR", "ADMIN")

                        // Read-only data endpoints — viewer+ (legacy flat shape — removed per-wave)
                        .requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/agents/*/metrics").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/agents").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/agents/events-log").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/routes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.GET, "/api/v1/stats/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")

                        // Runtime management (OPERATOR+) — legacy flat shape
                        .requestMatchers("/api/v1/apps/**").hasAnyRole("OPERATOR", "ADMIN")

                        // Alerting — env-scoped reads (VIEWER+)
                        .requestMatchers(HttpMethod.GET,    "/api/v1/environments/*/alerts/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        // Alerting — rule mutations (OPERATOR+)
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/rules/**").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.PUT,    "/api/v1/environments/*/alerts/rules/**").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/rules/**").hasAnyRole("OPERATOR", "ADMIN")
                        // Alerting — silence mutations (OPERATOR+)
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.PUT,    "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN")
                        // Alerting — ack/read/bulk-ack (VIEWER+ self-service)
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/*/ack").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/*/read").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/bulk-read").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/bulk-ack").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
                        // Alerting — soft-delete / restore (OPERATOR+)
                        .requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/*").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/bulk-delete").hasAnyRole("OPERATOR", "ADMIN")
                        .requestMatchers(HttpMethod.POST,   "/api/v1/environments/*/alerts/*/restore").hasAnyRole("OPERATOR", "ADMIN")
                        // Alerting — notification retry (flat path; notification IDs globally unique)
                        .requestMatchers(HttpMethod.POST,   "/api/v1/alerts/notifications/*/retry").hasAnyRole("OPERATOR", "ADMIN")

                        // Outbound connections: list/get allow OPERATOR (method-level @PreAuthorize gates mutations)
                        .requestMatchers(HttpMethod.GET, "/api/v1/admin/outbound-connections", "/api/v1/admin/outbound-connections/**").hasAnyRole("OPERATOR", "ADMIN")

                        // Admin endpoints (catch-all)
                        .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")

                        // Everything else requires authentication
                        .anyRequest().authenticated()
                )
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                )
                .addFilterBefore(
                        new JwtAuthenticationFilter(jwtService, registryService, oidcDecoder, userRepository, serverMetrics),
                        UsernamePasswordAuthenticationFilter.class
                );

        return http.build();
    }

    /**
     * Builds an OIDC {@link JwtDecoder} for validating external access tokens.
     * Discovers JWKS URI from the OIDC well-known endpoint. Handles Logto's
     * {@code at+jwt} token type (RFC 9068) by accepting any JWT type.
     */
    private JwtDecoder buildOidcDecoder(SecurityProperties properties) throws Exception {
        String issuerUri = properties.getOidc().getIssuerUri();

        // Resolve JWKS URI: use explicit config if set, otherwise discover from OIDC metadata.
        URL jwksUri;
        String jwkSetUri = properties.getOidc().getJwkSetUri();
        if (jwkSetUri != null && !jwkSetUri.isBlank()) {
            jwksUri = new URI(jwkSetUri).toURL();
            log.info("Using explicit JWKS URI: {}", jwksUri);
        } else {
            OIDCProviderMetadata metadata = OidcProviderHelper.fetchMetadata(
                    issuerUri, properties.getOidc().isTlsSkipVerify());
            jwksUri = metadata.getJWKSetURI().toURL();
        }

        JWKSource<SecurityContext> jwkSource = OidcProviderHelper.buildJwkSource(
                jwksUri, properties.getOidc().isTlsSkipVerify());
        var keySelector = new JWSVerificationKeySelector<SecurityContext>(
                OidcProviderHelper.SUPPORTED_ALGORITHMS, jwkSource);
        var processor = new DefaultJWTProcessor<SecurityContext>();
        processor.setJWSKeySelector(keySelector);
        // Accept any JWT type — Logto uses "at+jwt" (RFC 9068)
        processor.setJWSTypeVerifier((type, ctx) -> { });
        var decoder = new NimbusJwtDecoder(processor);

        // Validate issuer + optionally audience
        OAuth2TokenValidator<Jwt> validators;
        String audience = properties.getOidc().getAudience();
        if (audience != null && !audience.isBlank()) {
            validators = new DelegatingOAuth2TokenValidator<>(
                    JwtValidators.createDefaultWithIssuer(issuerUri),
                    new JwtClaimValidator<List<String>>("aud",
                            aud -> aud != null && aud.contains(audience))
            );
        } else {
            validators = JwtValidators.createDefaultWithIssuer(issuerUri);
        }
        decoder.setJwtValidator(validators);

        log.info("OIDC decoder initialized: jwks={}", jwksUri);
        return decoder;
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource(SecurityProperties properties) {
        CorsConfiguration config = new CorsConfiguration();
        String corsOrigins = properties.getCorsAllowedOrigins();
        if (corsOrigins != null && !corsOrigins.isBlank()) {
            config.setAllowedOrigins(Arrays.stream(corsOrigins.split(","))
                    .map(String::trim).filter(s -> !s.isEmpty()).toList());
        } else {
            String origin = properties.getUiOrigin();
            config.setAllowedOrigins(List.of(
                    origin != null && !origin.isBlank() ? origin : "http://localhost:5173"));
        }
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

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