package cn.qihangerp.security.external;

import cn.qihangerp.common.AjaxResult;
import cn.qihangerp.common.enums.HttpStatus;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.List;
import java.util.Objects;

/**
 * External system auth for /external/** endpoints.
 *
 * Header contract (frozen):
 * - X-Api-Key: AK
 * - X-Timestamp: ms
 * - X-Nonce: random string
 * - X-Signature: Base64(HMAC-SHA256(SK, canonicalString))
 *
 * canonicalString:
 * METHOD + "\n" + PATH + "\n" + timestamp + "\n" + nonce + "\n" + sha256Hex(rawBodyBytes)
 */
public class ExternalAkSkAuthenticationFilter extends OncePerRequestFilter {
    private static final String HEADER_API_KEY = "X-Api-Key";
    private static final String HEADER_TIMESTAMP = "X-Timestamp";
    private static final String HEADER_NONCE = "X-Nonce";
    private static final String HEADER_SIGNATURE = "X-Signature";

    private final ExternalAkSkProperties props;
    private final ExternalRequestCaches caches;

    public ExternalAkSkAuthenticationFilter(
            ExternalAkSkProperties props,
            ExternalRequestCaches caches
    ) {
        this.props = Objects.requireNonNull(props, "props");
        this.caches = Objects.requireNonNull(caches, "caches");
    }

    public static ExternalAkSkAuthenticationFilter createDefault(ExternalAkSkProperties props) {
        return new ExternalAkSkAuthenticationFilter(
                props,
                new ExternalRequestCaches(Clock.systemUTC())
        );
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        return path == null || !path.startsWith("/external/");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CachedBodyHttpServletRequest wrapped = request instanceof CachedBodyHttpServletRequest
                ? (CachedBodyHttpServletRequest) request
                : new CachedBodyHttpServletRequest(request);

        long nowMs = caches.nowMs();

        String apiKey = wrapped.getHeader(HEADER_API_KEY);
        String tsStr = wrapped.getHeader(HEADER_TIMESTAMP);
        String nonce = wrapped.getHeader(HEADER_NONCE);
        String signature = wrapped.getHeader(HEADER_SIGNATURE);

        if (!StringUtils.hasText(apiKey) || !StringUtils.hasText(tsStr) || !StringUtils.hasText(nonce) || !StringUtils.hasText(signature)) {
            writeUnauthorized(response, "缺少鉴权头");
            return;
        }

        if (!apiKey.equals(props.getApiKey())) {
            writeForbidden(response, "AK无效");
            return;
        }

        long tsMs;
        try {
            tsMs = Long.parseLong(tsStr);
        } catch (NumberFormatException ex) {
            writeForbidden(response, "timestamp格式错误");
            return;
        }

        long skew = Math.abs(nowMs - tsMs);
        if (skew > props.getTimestampSkewMs()) {
            writeForbidden(response, "timestamp过期");
            return;
        }

        // anti-replay: use timestampSkew window as nonce TTL
        if (!caches.tryUseNonce(nonce, nowMs, props.getTimestampSkewMs())) {
            writeForbidden(response, "nonce重放");
            return;
        }

        byte[] bodyBytes = wrapped.getCachedBody();

        String bodySha256Hex = ExternalRequestCaches.sha256Hex(bodyBytes == null ? new byte[0] : bodyBytes);
        String method = wrapped.getMethod() == null ? "" : wrapped.getMethod().toUpperCase();
        String path = wrapped.getRequestURI() == null ? "" : wrapped.getRequestURI();
        String canonical = method + "\n" + path + "\n" + tsStr + "\n" + nonce + "\n" + bodySha256Hex;

        String expected = ExternalRequestCaches.hmacSha256Base64(props.getSecretKey(), canonical);
        if (!expected.equals(signature)) {
            writeForbidden(response, "签名错误");
            return;
        }

        // mark authenticated for Spring Security
        var auth = new UsernamePasswordAuthenticationToken(
                "external",
                null,
                List.of(new SimpleGrantedAuthority("ROLE_EXTERNAL"))
        );
        SecurityContextHolder.getContext().setAuthentication(auth);

        filterChain.doFilter(wrapped, response);
    }

    private void writeUnauthorized(HttpServletResponse response, String msg) throws IOException {
        response.setStatus(HttpStatus.UNAUTHORIZED);
        writeJson(response, AjaxResult.error(HttpStatus.UNAUTHORIZED, msg));
    }

    private void writeForbidden(HttpServletResponse response, String msg) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN);
        writeJson(response, AjaxResult.error(HttpStatus.FORBIDDEN, msg));
    }

    private void writeJson(HttpServletResponse response, AjaxResult body) throws IOException {
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(JSON.toJSONString(body));
    }

    /**
     * Cache request body and allow downstream re-read.
     */
    private static class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
        private final byte[] cachedBody;

        CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
            super(request);
            this.cachedBody = request.getInputStream().readAllBytes();
        }

        byte[] getCachedBody() {
            return cachedBody;
        }

        @Override
        public ServletInputStream getInputStream() {
            ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return bais.available() == 0;
                }

                @Override
                public boolean isReady() {
                    return true;
                }

                @Override
                public void setReadListener(ReadListener readListener) {
                    // no-op
                }

                @Override
                public int read() {
                    return bais.read();
                }
            };
        }

        @Override
        public BufferedReader getReader() {
            return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
        }
    }
}

