package tech.powerjob.common.utils;
/*
Copyright [2020] [PowerJob]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

import tech.powerjob.common.PowerJobDKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;

import static java.util.Collections.emptyList;

/**
 * IP and Port Helper for RPC
 *
 * @author from dubbo, optimize by tjq
 * @since 2020/8/8
 */
@Slf4j
public class NetUtils {

    /**
     * returned port range is [30000, 39999]
     */
    private static final int RND_PORT_START = 30000;
    private static final int RND_PORT_END = 65535;

    private static volatile String HOST_ADDRESS;
    private static final String LOCALHOST_VALUE = "127.0.0.1";
    private static volatile InetAddress LOCAL_ADDRESS = null;
    private static final Pattern IP_PATTERN = Pattern.compile("\\d{1,3}(\\.\\d{1,3}){3,5}$");
    private static final String ANY_HOST_VALUE = "0.0.0.0";

    private NetUtils() {
    }

    public static int getRandomPort() {
        return ThreadLocalRandom.current().nextInt(RND_PORT_START, RND_PORT_END);
    }

    /**
     * 检测某个 IP 端口是否可用
     * @param ip IP
     * @param port 端口
     * @return 是否可用
     */
    public static boolean checkIpPortAvailable(String ip, int port) {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(ip, port), 1000);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取本机 IP 地址
     *
     * @return 本机 IP 地址
     */
    public static String getLocalHost() {
        return getLocalHostWithNetworkInterfaceChecker(null);
    }

    public static String getLocalHostWithNetworkInterfaceChecker(NetworkInterfaceChecker networkInterfaceChecker) {
        if (HOST_ADDRESS != null) {
            return HOST_ADDRESS;
        }

        String addressFromJVM = System.getProperty(PowerJobDKey.BIND_LOCAL_ADDRESS);
        if (StringUtils.isNotEmpty(addressFromJVM)) {
            log.info("[Net] use address from[{}]: {}", PowerJobDKey.BIND_LOCAL_ADDRESS, addressFromJVM);
            return HOST_ADDRESS = addressFromJVM;
        }

        InetAddress address = getLocalAddress(networkInterfaceChecker);
        if (address != null) {
            return HOST_ADDRESS = address.getHostAddress();
        }
        return LOCALHOST_VALUE;
    }

    /**
     * 隔离调用 scope，核心场景才能直接调用 getLocalHost，方便查看使用点
     * @return IP
     */
    public static String getLocalHost4Test() {
        return getLocalHost();
    }

    /**
     * Find first valid IP from local network card
     *
     * @return first valid local IP
     */
    public static InetAddress getLocalAddress(NetworkInterfaceChecker networkInterfaceChecker) {
        if (LOCAL_ADDRESS != null) {
            return LOCAL_ADDRESS;
        }
        InetAddress localAddress = getLocalAddress0(networkInterfaceChecker);
        LOCAL_ADDRESS = localAddress;
        return localAddress;
    }

    private static InetAddress getLocalAddress0(NetworkInterfaceChecker networkInterfaceChecker) {
        // @since 2.7.6, choose the {@link NetworkInterface} first
        try {
            InetAddress addressOp = getFirstReachableInetAddress( findNetworkInterface(networkInterfaceChecker));
            if (addressOp != null) {
                return addressOp;
            }
        } catch (Throwable e) {
            log.warn("[Net] getLocalAddress0 failed.", e);
        }

        InetAddress localAddress = null;
        try {
            localAddress = InetAddress.getLocalHost();
            Optional<InetAddress> addressOp = toValidAddress(localAddress);
            if (addressOp.isPresent()) {
                return addressOp.get();
            }
        } catch (Throwable e) {
            log.warn("[Net] getLocalAddress0 failed.", e);
        }


        return localAddress;
    }

    private static InetAddress getFirstReachableInetAddress(NetworkInterface networkInterface) {

        if(networkInterface == null ){
            return null;
        }
        Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
        while (addresses.hasMoreElements()) {
            Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
            if (addressOp.isPresent()) {
                try {
                    if (addressOp.get().isReachable(100)) {
                        return addressOp.get();
                    }
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        return null;
    }

    /**
     * Get the suitable {@link NetworkInterface}
     *
     * @return If no {@link NetworkInterface} is available , return <code>null</code>
     * @since 2.7.6
     */
    public static NetworkInterface findNetworkInterface(NetworkInterfaceChecker networkInterfaceChecker) {

        List<NetworkInterface> validNetworkInterfaces = emptyList();
        try {
            validNetworkInterfaces = getValidNetworkInterfaces();
        } catch (Throwable e) {
            log.warn("[Net] findNetworkInterface failed", e);
        }

        // sort by interface index, the smaller is preferred.
         validNetworkInterfaces.sort(Comparator.comparingInt(NetworkInterface::getIndex));

        // Try to find the preferred one
        for (NetworkInterface networkInterface : validNetworkInterfaces) {
            if (isPreferredNetworkInterface(networkInterface)) {
                log.info("[Net] use preferred network interface: {}", networkInterface);
                return networkInterface;
            }
            if (isPassedCheckNetworkInterface(networkInterface, networkInterfaceChecker)) {
                log.info("[Net] use PassedCheckNetworkInterface: {}", networkInterface);
                return networkInterface;
            }
        }
        // If not found, try to get the first one
        for (NetworkInterface networkInterface : validNetworkInterfaces) {
            InetAddress addressOp = getFirstReachableInetAddress(networkInterface);
            if (addressOp != null) {
                return networkInterface;
            }
        }

        return first(validNetworkInterfaces);
    }

    /**
     * 通过用户方法判断是否为目标网卡
     * @param networkInterface networkInterface
     * @param networkInterfaceChecker 判断方法
     * @return true or false
     */
    static boolean isPassedCheckNetworkInterface(NetworkInterface networkInterface, NetworkInterfaceChecker networkInterfaceChecker) {
        if (networkInterfaceChecker == null) {
            return false;
        }
        log.info("[Net] try to choose NetworkInterface by NetworkInterfaceChecker, current NetworkInterface: {}", networkInterface);
        try {
            return networkInterfaceChecker.ok(networkInterface, getFirstReachableInetAddress(networkInterface));
        } catch (Exception e) {
            log.warn("[Net] isPassedCheckerNetworkInterface failed, current networkInterface: {}", networkInterface, e);
        }
        return false;
    }

    private static Optional<InetAddress> toValidAddress(InetAddress address) {
        if (address instanceof Inet6Address) {
            Inet6Address v6Address = (Inet6Address) address;
            if (isPreferIPV6Address()) {
                return Optional.ofNullable(normalizeV6Address(v6Address));
            }
        }
        if (isValidV4Address(address)) {
            return Optional.of(address);
        }
        return Optional.empty();
    }

    /**
     * Check if an ipv6 address
     *
     * @return true if it is reachable
     */
    static boolean isPreferIPV6Address() {
        return Boolean.getBoolean("java.net.preferIPv6Addresses");
    }

    static boolean isValidV4Address(InetAddress address) {
        if (address == null || address.isLoopbackAddress()) {
            return false;
        }

        String name = address.getHostAddress();
        return (name != null
                && IP_PATTERN.matcher(name).matches()
                && !ANY_HOST_VALUE.equals(name)
                && !LOCALHOST_VALUE.equals(name));
    }

    /**
     * normalize the ipv6 Address, convert scope name to scope id.
     * e.g.
     * convert
     * fe80:0:0:0:894:aeec:f37d:23e1%en0
     * to
     * fe80:0:0:0:894:aeec:f37d:23e1%5
     * <p>
     * The %5 after ipv6 address is called scope id.
     * see java doc of {@link Inet6Address} for more details.
     *
     * @param address the input address
     * @return the normalized address, with scope id converted to int
     */
    static InetAddress normalizeV6Address(Inet6Address address) {
        String addr = address.getHostAddress();
        int i = addr.lastIndexOf('%');
        if (i > 0) {
            try {
                return InetAddress.getByName(addr.substring(0, i) + '%' + address.getScopeId());
            } catch (UnknownHostException e) {
                // ignore
                log.debug("Unknown IPV6 address: ", e);
            }
        }
        return address;
    }

    /**
     * Get the valid {@link NetworkInterface network interfaces}
     *
     * @return non-null
     * @throws SocketException SocketException if an I/O error occurs.
     * @since 2.7.6
     */
    private static List<NetworkInterface> getValidNetworkInterfaces() throws SocketException {
        List<NetworkInterface> validNetworkInterfaces = new LinkedList<>();
        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface networkInterface = interfaces.nextElement();
            // ignore
            if (ignoreNetworkInterface(networkInterface)) {
                continue;
            }
            // 根据用户 -D 参数忽略网卡
            if (ignoreInterfaceByConfig(networkInterface.getDisplayName()) || ignoreInterfaceByConfig(networkInterface.getName())) {
                continue;
            }
            validNetworkInterfaces.add(networkInterface);
        }
        return validNetworkInterfaces;
    }

    /**
     * @param networkInterface {@link NetworkInterface}
     * @return if the specified {@link NetworkInterface} should be ignored, return <code>true</code>
     * @throws SocketException SocketException if an I/O error occurs.
     * @since 2.7.6
     */
    private static boolean ignoreNetworkInterface(NetworkInterface networkInterface) throws SocketException {
        return networkInterface == null
                || networkInterface.isLoopback()
                || networkInterface.isVirtual()
                || !networkInterface.isUp();
    }

    /**
     * Take the first element from the specified collection
     *
     * @param values the collection object
     * @param <T>    the type of element of collection
     * @return if found, return the first one, or <code>null</code>
     * @since 2.7.6
     */
    public static <T> T first(Collection<T> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        if (values instanceof List) {
            List<T> list = (List<T>) values;
            return list.get(0);
        } else {
            return values.iterator().next();
        }
    }

    /**
     * Is preferred {@link NetworkInterface} or not
     *
     * @param networkInterface {@link NetworkInterface}
     * @return if the name of the specified {@link NetworkInterface} matches
     * the property value from {@link PowerJobDKey#PREFERRED_NETWORK_INTERFACE}, return <code>true</code>,
     * or <code>false</code>
     */
    public static boolean isPreferredNetworkInterface(NetworkInterface networkInterface) {
        String preferredNetworkInterface = System.getProperty(PowerJobDKey.PREFERRED_NETWORK_INTERFACE);
        if (Objects.equals(networkInterface.getDisplayName(), preferredNetworkInterface)) {
            return true;
        }
        // 兼容直接使用网卡名称的情况，比如 Realtek PCIe GBE Family Controller
        return Objects.equals(networkInterface.getName(), preferredNetworkInterface);
    }

    public static Pair<String, Integer> splitAddress2IpAndPort(String address) {
        String[] split = address.split(":");
        return Pair.of(split[0], Integer.valueOf(split[1]));
    }

    static boolean ignoreInterfaceByConfig(String interfaceName) {
        String regex = System.getProperty(PowerJobDKey.IGNORED_NETWORK_INTERFACE_REGEX);
        if (StringUtils.isBlank(regex)) {
            return false;
        }
        if (interfaceName.matches(regex)) {
            log.info("[Net] ignore network interface: {} by regex({})", interfaceName, regex);
            return true;
        }
        return false;
    }

    @FunctionalInterface
    public interface NetworkInterfaceChecker {
        boolean ok(NetworkInterface networkInterface, InetAddress inetAddress);
    }
}
