package ru.kirillius.pf.sdn.core.Util;

import lombok.Getter;
import lombok.SneakyThrows;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;

import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import java.util.stream.Collectors;


/**
 * Helper methods for IPv4 address manipulation and subnet aggregation logic.
 */
public class IPv4Util {

    private IPv4Util() {
    }

    private static final Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");

    /**
     * Ensures the supplied string is a valid IPv4 address.
     */
    public static void validateAddress(String address) {
        if (!pattern.matcher(address).matches()) {
            throw new IllegalArgumentException("Invalid IPv4 address: " + address);
        }
    }

    /**
     * Ensures the prefix length is within the allowed range 0..32.
     */
    public static void validatePrefix(int prefix) {
        if (prefix < 0 || prefix > 32) {
            throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
        }
    }


    /**
     * Converts a dotted IPv4 address into its numeric representation.
     */
    @SneakyThrows
    public static long ipAddressToLong(String address) {
        validateAddress(address);
        var ip = InetAddress.getByName(address);
        var bytes = ip.getAddress();
        var result = 0L;
        for (var b : bytes) {
            result = (result << 8) | (b & 0xFF);
        }
        return result;
    }

    /**
     * Returns a bit mask representing the provided prefix length.
     */
    public static long calculateMask(int prefixLength) {
        validatePrefix(prefixLength);
        return 0xFFFFFFFFL << (32 - prefixLength);
    }


    /**
     * Converts a numeric IPv4 address into dotted notation.
     */
    public static String longToIpAddress(long ipLong) {
        if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
            throw new IllegalArgumentException("Address number should be in range 0 - 4294967295");
        }
        return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
    }

    /**
     * Formats a numeric mask into dotted notation.
     */
    public static String maskToString(long maskLong) {
        return String.format("%d.%d.%d.%d",
                (maskLong >> 24) & 0xff,
                (maskLong >> 16) & 0xff,
                (maskLong >> 8) & 0xff,
                maskLong & 0xff);
    }

    /**
     * Result contract for subnet summarisation operations.
     */
    public interface SummarisationResult {
        /**
         * Returns the resulting set of subnets after summarisation.
         */
        List<IPv4Subnet> getResult();

        /**
         * Returns the original subnets that were merged during summarisation.
         */
        Set<IPv4Subnet> getMergedSubnets();
    }

    /**
     * Performs subnet merging strategies based on overlap and utilisation heuristics.
     */
    private static class SubnetSummaryUtility implements SummarisationResult {

        @Getter
        private final List<IPv4Subnet> result;
        private final Collection<IPv4Subnet> source;
        @Getter
        private final Set<IPv4Subnet> mergedSubnets = new HashSet<>();


        /**
         * Builds the summarisation utility with the supplied subnets and usage threshold.
         */
        public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) {
            source = subnets;
            result = new ArrayList<>(subnets);
            summaryOverlapped();
            mergeNeighbours();
            summaryWithUsage(usePercentage > 50 ? usePercentage : 51);
            summaryOverlapped();
            result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
        }

        /**
         * Removes redundant subnets wholly covered by other subnets.
         */
        private void summaryOverlapped() {
            if (result.size() < 2) {
                return;
            }
            //check subnets overlaps
            var overlapped = new ArrayList<IPv4Subnet>();
            var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList();
            orderedByPrefix.stream()
                    .filter(subnet -> subnet.getPrefixLength() > 32)
                    .forEach(parent -> orderedByPrefix.forEach(subnet -> {
                        if (subnet.equals(parent)) {
                            return;
                        }
                        if (parent.overlaps(subnet)) {
                            overlapped.add(subnet);
                            if (source.contains(subnet)) {
                                mergedSubnets.add(subnet);
                            }
                        }
                    }));
            overlapped.forEach(result::remove);
        }

        /**
         * Attempts to merge adjacent subnets into larger ones while preserving coverage.
         */
        private void mergeNeighbours() {
            if (result.size() < 2) {
                return;
            }
            var availableLengths = result.stream().map(IPv4Subnet::getPrefixLength).collect(Collectors.toSet());
            for (var length = 32; length > 0; length--) {
                if (!availableLengths.contains(length)) {
                    continue;
                }
                var finalLength = length;
                var largerPrefixLength = length - 1;
                var largerPrefixMask = IPv4Util.calculateMask(largerPrefixLength);


                var selectedSubnets = result.stream().filter(subnet -> subnet.getPrefixLength() == finalLength).sorted(Comparator.comparing(IPv4Subnet::getLongAddress)).toList();
                //проверяем является ли адрес подсети таким же как адрес подсети с перфиксом -1
                for (var i = 0; i < selectedSubnets.size() - 1; i++) {
                    var subnet = selectedSubnets.get(i);
                    var next = selectedSubnets.get(i + 1);
                    var firstAddress = subnet.getLongAddress();
                    if (firstAddress != (firstAddress & largerPrefixMask)) {
                        continue;
                    }
                    var largerSubnet = new IPv4Subnet(subnet.getAddress(), largerPrefixLength);
                    if (largerSubnet.overlaps(next)) {
                        //если подсеть перекрывает соседнюю, то удаляем обе и добавляем новую
                        availableLengths.add(largerPrefixLength);
                        result.remove(subnet);
                        result.remove(next);
                        result.add(largerSubnet);
                        if (source.contains(subnet)) {
                            mergedSubnets.add(subnet);
                        }
                        if (source.contains(next)) {
                            mergedSubnets.add(next);
                        }
                        i++;
                    }
                }
            }
        }

        /**
         * Returns the smallest prefix length currently present in the result set.
         */
        private int findMinPrefixLength() {
            return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
        }

        /**
         * Returns the largest prefix length currently present in the result set.
         */
        private int findMaxPrefixLength() {
            return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
        }

        /**
         * Returns the smallest network address found in the result set.
         */
        private long findMinAddress() {
            return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
        }

        /**
         * Returns the largest network address found in the result set.
         */
        private long findMaxAddress() {
            return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
        }

        /**
         * Produces candidate subnets capable of covering the current set at the given prefix length.
         */
        private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) {
            //создаём подсети-кандидаты, которые покроют наш список
            var maxAddress = findMaxAddress();

            var mask = IPv4Util.calculateMask(prefixLength);
            var firstAddress = (findMinAddress() & mask);
            var lastAddress = (maxAddress & mask);

            var candidates = new ArrayList<IPv4Subnet>();
            var candidateAddress = firstAddress;
            do {
                var candidate = new IPv4Subnet(candidateAddress, prefixLength);
                candidates.add(candidate);
                if (candidates.size() > result.size()) {
                    throw new IllegalStateException("Too many IPv4 addresses when trying to summary " + result.size() + " subnets");
                }
                //поиск следующего адреса кандидата
                var nextAddress = candidateAddress + candidate.count();
                var nextSubnet = result.stream().filter(subnet -> {
                    var address = subnet.getLongAddress();
                    return subnet.getPrefixLength() > prefixLength && address >= nextAddress && address <= maxAddress;
                }).min(Comparator.comparingLong(IPv4Subnet::getLongAddress)).stream().findFirst();
                if (nextSubnet.isEmpty()) {
                    break;
                }
                candidateAddress = IPv4Util.createSubnetOverlapping(nextSubnet.get().getLongAddress(), prefixLength).getLongAddress();
            } while (candidateAddress <= lastAddress);
            return candidates;
        }

        /**
         * Tests whether the candidate subnet meets utilisation requirements and performs the merge.
         */
        private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
            if (result.contains(candidate)) {
                return false;
            }
            var min = candidate.getLongAddress();
            var max = candidate.getLongAddress() + candidate.count() - 1;
            var overlapped = new ArrayList<IPv4Subnet>();
            var used = new AtomicLong(0L);
            result.forEach(child -> {
                if (child.getLongAddress() < min || child.getLongAddress() > max) {
                    return;
                }

                if (candidate.overlaps(child)) {
                    overlapped.add(child);
                    used.addAndGet(child.count());
                }
            });

            if (100.0 * used.get() / candidate.count() >= usePercentage) {
                //подсеть подходит под критерий
                overlapped.forEach(subnet -> {
                    if (source.contains(subnet)) {
                        mergedSubnets.add(subnet);
                    }
                });
                result.removeAll(overlapped);
                result.add(candidate);
                return true;
            }
            return false;
        }

        /**
         * Iteratively merges subnets that satisfy the utilisation threshold.
         */
        private void summaryWithUsage(int usePercentage) {
            if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
                return;
            }

            var found = new AtomicBoolean();
            do {
                found.set(false);
                if (result.size() < 2) {
                    break;
                }
                var prefixMin = findMinPrefixLength();
                var prefixMax = findMaxPrefixLength();
                for (var testPrefixLength = prefixMin - 1; testPrefixLength < prefixMax; testPrefixLength++) {
                    //создаём подсети-кандидаты, которые покроют наш список
                    var candidates = findMergeCandidatesForPrefixLength(testPrefixLength);

                    candidates.forEach(candidate -> {
                        if (testCandidate(candidate, usePercentage)) {
                            found.set(true);
                        }
                    });

                    if (candidates.stream().anyMatch(result::contains)) {
                        //если был добавлен хотя бы 1 кандидат, то нужно пересчитать maxPrefix
                        prefixMax = result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
                    }
                }
            } while (found.get());
        }
    }

    /**
     * Summaries the provided subnets using merging heuristics and utilisation thresholds.
     */
    public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
        return new SubnetSummaryUtility(subnets, usePercentage);
    }

    /**
     * Creates a subnet covering the given address at the specified prefix length.
     */
    private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
        return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
    }

    /**
     * Returns the number of addresses represented by a prefix length.
     */
    public static long calculateCountForPrefixLength(long prefixLength) {
        return 1L << (32L - prefixLength);
    }


}
