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.rbac.SystemRole;
import com.cameleer.server.core.security.JwtService;
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
import com.cameleer.server.core.security.UserInfo;
import com.cameleer.server.core.security.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.Instant;
import java.util.List;

/**
 * JWT authentication filter that extracts and validates JWT tokens from
 * the {@code Authorization: Bearer} header or the {@code token} query parameter.
 * <p>
 * Tries internal HMAC validation first (agents, local users). If that fails and an
 * OIDC {@link JwtDecoder} is configured, falls back to OIDC token validation
 * (SaaS M2M tokens, external OIDC users). Scope-based role mapping for OIDC tokens.
 * <p>
 * Not annotated {@code @Component} -- constructed explicitly in {@link SecurityConfig}
 * to avoid double filter registration.
 */
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
    private static final String BEARER_PREFIX = "Bearer ";
    public static final String JWT_RESULT_ATTR = "cameleer.jwt.result";

    private final JwtService jwtService;
    private final AgentRegistryService agentRegistryService;
    private final JwtDecoder oidcDecoder;
    private final UserRepository userRepository;
    private final ServerMetrics serverMetrics;

    public JwtAuthenticationFilter(JwtService jwtService,
                                    AgentRegistryService agentRegistryService,
                                    JwtDecoder oidcDecoder,
                                    UserRepository userRepository,
                                    ServerMetrics serverMetrics) {
        this.jwtService = jwtService;
        this.agentRegistryService = agentRegistryService;
        this.oidcDecoder = oidcDecoder;
        this.userRepository = userRepository;
        this.serverMetrics = serverMetrics;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {
        String token = extractToken(request);

        if (token != null) {
            if (tryInternalToken(token, request)) {
                chain.doFilter(request, response);
                return;
            }
            if (oidcDecoder != null) {
                tryOidcToken(token, request);
            }
        }

        chain.doFilter(request, response);
    }

    private boolean tryInternalToken(String token, HttpServletRequest request) {
        try {
            JwtValidationResult result = jwtService.validateAccessToken(token);
            String subject = result.subject();

            // Token revocation check: reject tokens issued before revocation timestamp
            if (subject.startsWith("user:") && result.issuedAt() != null) {
                userRepository.findById(subject).ifPresent(user -> {
                    Instant revoked = user.tokenRevokedBefore();
                    if (revoked != null && result.issuedAt().isBefore(revoked)) {
                        serverMetrics.recordAuthFailure("revoked");
                        throw new com.cameleer.server.core.security.InvalidTokenException("Token revoked");
                    }
                });
            }

            List<String> roles = result.roles();
            if (!subject.startsWith("user:") && roles.isEmpty()) {
                roles = List.of("AGENT");
            }
            List<GrantedAuthority> authorities = toAuthorities(roles);
            UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(subject, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(auth);
            request.setAttribute(JWT_RESULT_ATTR, result);
            return true;
        } catch (Exception e) {
            log.debug("Internal JWT validation failed: {}", e.getMessage());
            return false;
        }
    }

    private void tryOidcToken(String token, HttpServletRequest request) {
        try {
            Jwt jwt = oidcDecoder.decode(token);
            List<String> roles = extractRolesFromScopes(jwt);
            List<GrantedAuthority> authorities = toAuthorities(roles);
            UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                            "oidc:" + jwt.getSubject(), null, authorities);
            SecurityContextHolder.getContext().setAuthentication(auth);
        } catch (Exception e) {
            log.debug("OIDC token validation failed: {}", e.getMessage());
            serverMetrics.recordAuthFailure("oidc_rejected");
        }
    }

    /**
     * Maps OAuth2 scopes to server RBAC roles.
     * Accepts both prefixed ({@code server:admin}) and bare ({@code admin}) scope names,
     * case-insensitive. Uses {@link SystemRole#normalizeScope} for consistent normalization.
     */
    private List<String> extractRolesFromScopes(Jwt jwt) {
        String scopeStr = jwt.getClaimAsString("scope");
        log.info("OIDC access token scopes: '{}', subject={}, all claims={}",
                scopeStr, jwt.getSubject(), jwt.getClaims().keySet());
        if (scopeStr == null || scopeStr.isBlank()) {
            log.warn("OIDC token has no 'scope' claim — defaulting to VIEWER");
            return List.of("VIEWER");
        }
        for (String scope : scopeStr.split(" ")) {
            String normalized = SystemRole.normalizeScope(scope);
            if ("ADMIN".equals(normalized)) return List.of("ADMIN");
            if ("OPERATOR".equals(normalized)) return List.of("OPERATOR");
            if ("VIEWER".equals(normalized)) return List.of("VIEWER");
        }
        log.warn("OIDC scopes '{}' contain no recognized role — defaulting to VIEWER", scopeStr);
        return List.of("VIEWER");
    }

    private List<GrantedAuthority> toAuthorities(List<String> roles) {
        return roles.stream()
                .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
                .toList();
    }

    private String extractToken(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
            return authHeader.substring(BEARER_PREFIX.length());
        }
        return request.getParameter("token");
    }
}
