package com.seer.teach.filter.auth;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.reactor.context.SaReactorHolder;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.json.JSONUtil;
import com.seer.teach.config.AuthProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Objects;

@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private AuthProperties authProperties;

    private PathPatternMatcher pathMatcher;

    private PermissionCacheManager cacheManager;

    private WebClient webClient;

    Retry retryStrategy = Retry.backoff(3, Duration.ofMillis(200))
            .maxBackoff(Duration.ofSeconds(3))
            .jitter(0.3)
            .filter(ex -> ex instanceof java.net.ConnectException || ex instanceof java.net.SocketTimeoutException)
            .doBeforeRetry(retry ->
                    log.warn("Retrying ({}/3) due to: {}",
                            retry.totalRetries() + 1,
                            retry.failure().getMessage())
            )
            .onRetryExhaustedThrow((spec, signal) ->
                    new ServiceUnavailableException("Service unavailable after retries", signal.failure())
            );

    public AuthFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction, AuthProperties authProperties, PathPatternMatcher pathMatcher, PermissionCacheManager cacheManager) {
        this.webClient = WebClient.builder().filter(lbFunction).build();
        this.authProperties = authProperties;
        this.pathMatcher = pathMatcher;
        this.cacheManager = cacheManager;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(SaReactorHolder.CHAIN_KEY, chain);

        SaReactorSyncHolder.setContext(exchange);

        // 1. 检查鉴权开关
        if (!authProperties.isEnabled()) {
            log.info("AuthFilter is disabled.");
            return chain.filter(exchange);
        }

        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        HttpMethod method = request.getMethod();

        // 2. 检查排除路径
        for (String pattern : authProperties.getExcludePatterns()) {
            if (pathMatcher.matches(pattern, path)) {
                log.info("AuthFilter pattern:{} path:{} is excluded.", pattern, path);
                return chain.filter(exchange);
            }
        }

        // 3. 从请求头中获取token
        String token = extractToken(request);
        // 如果没有token，则返回401
        if (token == null) {
            log.info("AuthFilter token is null.");
            return unauthorized(exchange);
        }
        log.info("AuthFilter token:{}", token);
        return Mono.fromCallable(() -> {
                    Object object = StpUtil.getLoginIdByToken(token);
                    if (Objects.isNull(object)) {
                        log.info("AuthFilter object is null.");
                        return unauthorized(exchange);
                    }
                    log.info("AuthFilter object:{}", object);
                    return object;
                })
                .subscribeOn(Schedulers.boundedElastic())
                .flatMap(loginId -> {
                    if (loginId instanceof String) {
                        try {
                            long userId = Long.parseLong((String) loginId);
                            log.info("AuthFilter userId:{}", userId);
                            return checkPermission(userId, path, method, exchange, chain);
                        } catch (NumberFormatException e) {
                            log.error("AuthFilter failed to parse userId: {}", loginId, e);
                            return unauthorized(exchange);
                        }
                    } else if (loginId instanceof Number) {
                        long userId = ((Number) loginId).longValue();
                        log.info("AuthFilter userId:{}", userId);
                        return checkPermission(userId, path, method, exchange, chain);
                    } else {
                        log.error("AuthFilter unsupported loginId type: {}", loginId.getClass());
                        return unauthorized(exchange);
                    }
                })
                .onErrorResume(NotLoginException.class, e -> unauthorized(exchange));
    }

    private String extractToken(ServerHttpRequest request) {
        return StpUtil.getTokenValue();
    }

    private Mono<Void> checkPermission(long userId, String path, HttpMethod method,
                                       ServerWebExchange exchange, GatewayFilterChain chain) {
        String cacheKey = "user:permission:" + userId;
        return cacheManager.getPermissions(cacheKey)
                .switchIfEmpty(Mono.defer(() ->
                        fetchPermissionsFromService(userId, cacheKey)))
                .flatMap(permissions -> {
                    if (hasPermission(permissions, path, method)) {
                        return chain.filter(exchange);
                    } else {
                        return forbidden(exchange);
                    }
                });
    }

    private Mono<List<Permission>> fetchPermissionsFromService(long userId, String cacheKey) {
        return webClient
                .get()
                .uri(authProperties.getAuthApi(), uriBuilder -> uriBuilder.queryParam("userId", userId).build())
                .retrieve()
                .bodyToMono(UserPermissionResponse.class)
                .retryWhen(retryStrategy)
                .doOnError(ex -> log.error("Final error after retries", ex))
                .doOnSuccess(response -> log.info("Request succeeded: {}", JSONUtil.toJsonStr(response)))
                .map(UserPermissionResponse::getPermissions)
                .flatMap(perms ->
                        cacheManager.updateCache(cacheKey, perms).thenReturn(perms));
    }

    private boolean hasPermission(List<Permission> permissions, String path, HttpMethod method) {
        if (CollectionUtils.isEmpty(permissions)) {
            log.info("AuthFilter permissions is Empty.");
            return false;
        }
        log.info("AuthFilter hasPermission:{} , path :{} method : {}", permissions.size(), path, method);
        return permissions.stream().anyMatch(perm -> {
            if (StringUtils.hasText(perm.getApiPath()) && StringUtils.hasText(perm.getHttpMethod())) {
                return pathMatcher.matches(perm.getApiPath(), path) &&
                        (perm.getHttpMethod().equalsIgnoreCase("ANY") ||
                                perm.getHttpMethod().equalsIgnoreCase(method.name()));
            }
            return false;
        });
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String body = "{\"error\": \"Unauthorized\", \"message\": \"Missing or invalid token\"}";
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }

    private Mono<Void> forbidden(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return -100;
    }


    static class ServiceUnavailableException extends RuntimeException {
        public ServiceUnavailableException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}