package com.magistr.app.config.auth;

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

/**
 * Использует X-Forwarded-For только когда запрос действительно пришёл от доверенного proxy.
 */
@Component
public class ClientIpResolver {

    private static final Logger log = LoggerFactory.getLogger(ClientIpResolver.class);
    private static final int MAX_FORWARDED_HEADER_LENGTH = 2_048;
    private static final int MAX_PROXY_HOPS = 20;
    private static final String UNKNOWN_ADDRESS = "неизвестен";

    private final List<CidrRange> trustedProxyRanges;

    public ClientIpResolver(@Value("${app.http.trusted-proxy-cidrs:}") String trustedProxyCidrs) {
        this.trustedProxyRanges = parseTrustedRanges(trustedProxyCidrs);
    }

    public String resolve(HttpServletRequest request) {
        Optional<InetAddress> remoteAddress = parseAddress(request.getRemoteAddr());
        String fallback = remoteAddress.map(InetAddress::getHostAddress).orElse(UNKNOWN_ADDRESS);
        if (remoteAddress.isEmpty() || !isTrusted(remoteAddress.get())) {
            return fallback;
        }

        String forwarded = request.getHeader("X-Forwarded-For");
        if (forwarded == null || forwarded.isBlank()) {
            return fallback;
        }
        if (forwarded.length() > MAX_FORWARDED_HEADER_LENGTH) {
            log.debug("Заголовок X-Forwarded-For отклонён: превышена допустимая длина");
            return fallback;
        }

        String[] hops = forwarded.split(",", -1);
        if (hops.length > MAX_PROXY_HOPS) {
            log.debug("Заголовок X-Forwarded-For отклонён: слишком длинная цепочка proxy");
            return fallback;
        }

        InetAddress leftmost = null;
        for (int index = hops.length - 1; index >= 0; index--) {
            Optional<InetAddress> hop = parseAddress(hops[index].trim());
            if (hop.isEmpty()) {
                log.debug("Заголовок X-Forwarded-For отклонён: обнаружен некорректный IP-адрес");
                return fallback;
            }
            leftmost = hop.get();
            if (!isTrusted(hop.get())) {
                return hop.get().getHostAddress();
            }
        }
        return leftmost == null ? fallback : leftmost.getHostAddress();
    }

    private boolean isTrusted(InetAddress address) {
        return trustedProxyRanges.stream().anyMatch(range -> range.contains(address));
    }

    private List<CidrRange> parseTrustedRanges(String rawRanges) {
        if (rawRanges == null || rawRanges.isBlank()) {
            return List.of();
        }
        List<CidrRange> ranges = new ArrayList<>();
        for (String rawRange : rawRanges.split(",")) {
            String value = rawRange.trim();
            if (!value.isEmpty()) {
                ranges.add(CidrRange.parse(value));
            }
        }
        return List.copyOf(ranges);
    }

    private static Optional<InetAddress> parseAddress(String rawAddress) {
        if (rawAddress == null) {
            return Optional.empty();
        }
        String value = rawAddress.trim().toLowerCase(Locale.ROOT);
        if (value.startsWith("[") && value.endsWith("]")) {
            value = value.substring(1, value.length() - 1);
        }
        if (value.isEmpty() || !value.matches("[0-9a-f:.]+")) {
            return Optional.empty();
        }
        try {
            return Optional.of(InetAddress.getByName(value));
        } catch (UnknownHostException ignored) {
            return Optional.empty();
        }
    }

    private record CidrRange(byte[] network, int prefixLength) {

        static CidrRange parse(String value) {
            String[] parts = value.split("/", -1);
            Optional<InetAddress> address = ClientIpResolver.parseAddress(parts[0]);
            if (address.isEmpty()) {
                throw new IllegalStateException("Некорректный адрес доверенного proxy: " + value);
            }
            int addressBits = address.get().getAddress().length * 8;
            int prefix;
            try {
                prefix = parts.length == 1 ? addressBits : Integer.parseInt(parts[1]);
            } catch (NumberFormatException exception) {
                throw new IllegalStateException("Некорректная маска доверенного proxy: " + value, exception);
            }
            if (parts.length > 2 || prefix < 0 || prefix > addressBits) {
                throw new IllegalStateException("Некорректная маска доверенного proxy: " + value);
            }
            return new CidrRange(mask(address.get().getAddress(), prefix), prefix);
        }

        boolean contains(InetAddress candidate) {
            byte[] candidateBytes = candidate.getAddress();
            return candidateBytes.length == network.length
                    && Arrays.equals(mask(candidateBytes, prefixLength), network);
        }

        private static byte[] mask(byte[] address, int prefix) {
            byte[] masked = Arrays.copyOf(address, address.length);
            int fullBytes = prefix / 8;
            int remainingBits = prefix % 8;
            if (fullBytes < masked.length && remainingBits > 0) {
                masked[fullBytes] &= (byte) (0xFF << (8 - remainingBits));
                fullBytes++;
            }
            Arrays.fill(masked, fullBytes, masked.length, (byte) 0);
            return masked;
        }
    }
}
