package ru.kirillius.pf.sdn.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.kirillius.pf.sdn.Util.IPv4Util;
import ru.kirillius.pf.sdn.api.Networking.DomainResolver;
import ru.kirillius.pf.sdn.config.BeanConfiguration;

import java.io.IOException;
import java.io.InputStream;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class DomainResolverService implements DomainResolver {
    private final static int SUCCESS_CODE = 0;
    private final BeanConfiguration config;
    private final ExecutorService executor;
    private final RuntimeConfigService configService;

    /**
     * Returns a list of IPv4 addresses resolved for the provided domain.
     */
    private static List<Inet4Address> lookup(String domain, Inet4Address server) throws IOException, InterruptedException {
        if (domain == null || domain.isBlank()) {
            throw new IllegalArgumentException("Domain must not be null or blank");
        }

        var processBuilder = new ProcessBuilder("nslookup", domain.trim(), server.getHostAddress().trim());
        processBuilder.redirectErrorStream(true);

        var process = processBuilder.start();
        try {
            if (!process.waitFor(10, TimeUnit.SECONDS)) {
                process.destroyForcibly();
                throw new InterruptedException("Request timed out for domain: " + domain);
            }

            var output = readStream(process.getInputStream());
            if (process.exitValue() != SUCCESS_CODE) {
                throw new IOException("nslookup failed for domain " + domain + ": " + output);
            }
            return extractIPv4(output);
        } finally {
            process.destroy();
        }

    }

    private static List<Inet4Address> extractIPv4(String output) {
        var addresses = new LinkedHashSet<Inet4Address>();
        var allowAddresses = false;

        for (var rawLine : output.split("\\R")) {
            var line = rawLine.trim();
            if (line.isEmpty()) {
                allowAddresses = false;
                continue;
            }

            if (line.startsWith("Name:")) {
                allowAddresses = true;
                continue;
            }

            if (!allowAddresses) {
                continue;
            }

            if (line.startsWith("Address:") || line.startsWith("Addresses:")) {
                var colonIndex = line.indexOf(':');
                if (colonIndex == -1 || colonIndex == line.length() - 1) {
                    continue;
                }

                var tokens = line.substring(colonIndex + 1).trim().split("\\s+");
                for (var token : tokens) {
                    try {
                        IPv4Util.validateAddress(token);
                        addresses.add((Inet4Address) Inet4Address.getByName(token));
                    } catch (IllegalArgumentException ignored) {
                        // skip invalid addresses such as IPv6
                    } catch (UnknownHostException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }

        return new ArrayList<>(addresses);
    }

    private static String readStream(InputStream stream) throws IOException {
        try (stream) {
            return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
        }
    }

    @Override
    public Future<List<Inet4Address>> getAddresses(String domain, Inet4Address server) {
        return getAddresses(domain, Collections.singletonList(server));
    }

    @Override
    public Future<List<Inet4Address>> getAddresses(String domain) {
        return getAddresses(domain, configService.getConfig().getDomainResolvers());
    }

    @Override
    public Future<List<Inet4Address>> getAddresses(String domain, List<Inet4Address> servers) {
        return executor.submit(() -> {
            var resolvers = configService.getConfig().getDomainResolvers();

            if (resolvers.isEmpty()) {
                throw new UnknownHostException("You're trying to resolve a domain without any resolvers");
            }

            for (var server : resolvers) {
                try {
                    return lookup(domain, server);
                } catch (IOException e) {
                    throw new UnknownHostException("Failed to resolve domain: " + domain);
                } catch (InterruptedException e) {
                    continue; //network error
                }
            }
            throw new UnknownHostException("Failed to resolve domain " + domain + " due to network error or something nasty");
        });
    }
}
