package com.malk.filter;

import com.alibaba.fastjson.JSON;
import com.malk.config.AuthConfigProperties;
import com.malk.core.NonceCache;
import com.malk.server.common.McR;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 请求鉴权过滤器（capability: request-auth + replay-guard 第一阶段）
 *
 * <p>执行顺序位于 {@link TraceIdFilter} 之后：</p>
 * <ol>
 *   <li>{@code enabled=false} → 直接放行</li>
 *   <li>路径在 {@code exempt-paths} → 放行</li>
 *   <li>校验 Header 完整性 → 缺失 401 {@code AUTH_HEADER_MISSING}</li>
 *   <li>时间戳窗口校验 → 超窗 401 {@code AUTH_TIMESTAMP_OUT_OF_WINDOW}</li>
 *   <li>Nonce 去重 → 重放 401 {@code AUTH_NONCE_REPLAYED}</li>
 *   <li>以上通过 → 写 MDC(authKey)，交给 {@link AuthInterceptor} 做签名校验</li>
 * </ol>
 *
 * <p>注意：签名校验不在此处做，因为 Filter 阶段无法拿到 {@code HandlerMethod} 识别
 * {@link NoAuth} 注解。签名校验延后到 Spring MVC {@code HandlerInterceptor.preHandle}。</p>
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class AuthFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger("point");

    public static final String H_KEY = "X-MJ-Key";
    public static final String H_TS = "X-MJ-Timestamp";
    public static final String H_NONCE = "X-MJ-Nonce";
    public static final String H_SIG = "X-MJ-Signature";
    public static final String MDC_AUTH_KEY = "authKey";

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @Autowired
    private AuthConfigProperties props;

    @Autowired
    private NonceCache nonceCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        if (!props.isEnabled()) {
            chain.doFilter(request, response);
            return;
        }
        if (isExempt(request.getServletPath())) {
            chain.doFilter(request, response);
            return;
        }

        String key = request.getHeader(H_KEY);
        String ts = request.getHeader(H_TS);
        String nonce = request.getHeader(H_NONCE);
        String sig = request.getHeader(H_SIG);
        if (empty(key) || empty(ts) || empty(nonce) || empty(sig)) {
            write(response, 401, "AUTH_HEADER_MISSING");
            log.warn("[Auth] header missing path={} key={}", request.getServletPath(), key);
            return;
        }

        long clientMs;
        try {
            clientMs = Long.parseLong(ts);
        } catch (NumberFormatException e) {
            write(response, 401, "AUTH_TIMESTAMP_OUT_OF_WINDOW");
            return;
        }
        long nowMs = System.currentTimeMillis();
        long windowMs = props.getWindow() * 1000L;
        if (Math.abs(nowMs - clientMs) > windowMs) {
            write(response, 401, "AUTH_TIMESTAMP_OUT_OF_WINDOW");
            log.warn("[Auth] timestamp out of window path={} key={} client={} server={}",
                    request.getServletPath(), key, clientMs, nowMs);
            return;
        }

        if (!nonceCache.putIfAbsent(nonce)) {
            write(response, 401, "AUTH_NONCE_REPLAYED");
            log.warn("[Auth] nonce replayed path={} key={} nonce={}", request.getServletPath(), key, nonce);
            return;
        }

        MDC.put(MDC_AUTH_KEY, key);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove(MDC_AUTH_KEY);
        }
    }

    private boolean isExempt(String path) {
        if (props.getExemptPaths() == null) return false;
        for (String pat : props.getExemptPaths()) {
            if (pathMatcher.match(pat, path)) return true;
        }
        return false;
    }

    private static boolean empty(String s) {
        return s == null || s.isEmpty();
    }

    private static void write(HttpServletResponse resp, int status, String code) throws IOException {
        resp.setStatus(status);
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=UTF-8");
        McR body = McR.error(code, code);
        resp.getOutputStream().write(JSON.toJSONString(body).getBytes(StandardCharsets.UTF_8));
    }
}
