package com.adguard.dnslibs.proxy;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class DnsProxySettings {
    /**
     * Specifies how to respond to blocked requests.
     *
     * A request is blocked if it matches a blocking AdBlock-style rule,
     * or a blocking hosts-style rule. A blocking hosts-style rule is
     * a hosts-style rule with a loopback or all-zeroes address.
     *
     * Requests matching a hosts-style rule with an address that is
     * neither loopback nor all-zeroes are always responded
     * with the address specified by the rule.
     */
    public enum BlockingMode {
        // MUST keep names and ordinals in sync with ag::DnsProxyBlockingMode

        /** Respond with REFUSED response code */
        REFUSED(4),

        /** Respond with NXDOMAIN response code */
        NXDOMAIN(1),

        /**
         * Respond with an address that is all-zeroes, or
         * a custom blocking address, if it is specified, or
         * an empty SOA response if request type is not A/AAAA.
         */
        ADDRESS(2);

        private final int code;
        BlockingMode(int code) { this.code = code; }

        public int getCode() { return code; }

        public static BlockingMode fromCode(int code) {
            for (final BlockingMode m : values()) {
                if (m.code == code) {
                    return m;
                }
            }
            throw new IllegalArgumentException("code is out of range");
        }
    }

    private List<UpstreamSettings> upstreams = new ArrayList<>();
    private List<UpstreamSettings> fallbacks = new ArrayList<>();
    private List<String> fallbackDomains = new ArrayList<>();
    private boolean detectSearchDomains;
    private Dns64Settings dns64;
    private long blockedResponseTtlSecs;
    private List<FilterParams> filterParams = new ArrayList<>();
    private List<ListenerSettings> listeners = new ArrayList<>();
    private OutboundProxySettings outboundProxy;
    private boolean ipv6Available;
    private boolean blockIpv6;
    private BlockingMode adblockRulesBlockingMode;
    private BlockingMode hostsRulesBlockingMode;
    private String customBlockingIpv4;
    private String customBlockingIpv6;
    private long dnsCacheSize;
    private boolean optimisticCache;
    private boolean enableDNSSECOK;
    private boolean enableRetransmissionHandling;
    private boolean blockEch;
    private boolean enableParallelUpstreamQueries;
    private boolean enableFallbackOnUpstreamsFailure;
    private boolean enableServfailOnUpstreamsFailure;
    private boolean enableHttp3;
    private long upstreamTimeoutMs;

    /**
     * @return Maximum number of cached responses
     */
    public long getDnsCacheSize() {
        return dnsCacheSize;
    }

    /**
     * @param dnsCacheSize Maximum number of cached responses
     */
    public void setDnsCacheSize(long dnsCacheSize) {
        this.dnsCacheSize = dnsCacheSize;
    }

    /**
     * @return Custom IPv4 address to return for filtered requests
     */
    public String getCustomBlockingIpv4() {
        return customBlockingIpv4;
    }

    /**
     * @param customBlockingIpv4 Custom IPv4 address to return for blocked requests instead of all-zeroes,
     *                           must be either empty/{@code null}, or a valid IPv4 address
     */
    public void setCustomBlockingIpv4(String customBlockingIpv4) {
        this.customBlockingIpv4 = customBlockingIpv4;
    }

    /**
     * @return Custom IPv6 address to return for filtered requests
     */
    public String getCustomBlockingIpv6() {
        return customBlockingIpv6;
    }

    /**
     * @param customBlockingIpv6 Custom IPv6 address to return for blcoked requests instead of all-zeroes,
     *                           must be either empty/{@code null}, or a valid IPv6 address
     */
    public void setCustomBlockingIpv6(String customBlockingIpv6) {
        this.customBlockingIpv6 = customBlockingIpv6;
    }

    /**
     * @return How to respond to requests blocked by AdBlock-style rules.
     */
    public BlockingMode getAdblockRulesBlockingMode() {
        return adblockRulesBlockingMode;
    }

    /**
     * @param adblockRulesBlockingMode How to respond to requests blocked by AdBlock-style rules.
     */
    public void setAdblockRulesBlockingMode(BlockingMode adblockRulesBlockingMode) {
        this.adblockRulesBlockingMode = adblockRulesBlockingMode;
    }

    /**
     * @return How to respond to requests blocked by hosts-style rules.
     */
    public BlockingMode getHostsRulesBlockingMode() {
        return hostsRulesBlockingMode;
    }

    /**
     * @param hostsRulesBlockingMode How to respond to requests blocked by hosts-style rules.
     */
    public void setHostsRulesBlockingMode(BlockingMode hostsRulesBlockingMode) {
        this.hostsRulesBlockingMode = hostsRulesBlockingMode;
    }

    /**
     * @return DNS upstreams settings list.
     */
    public List<UpstreamSettings> getUpstreams() {
        return upstreams;
    }

    /**
     * @param upstreams DNS upstreams settings list.
     */
    public void setUpstreams(List<UpstreamSettings> upstreams) {
        this.upstreams = new ArrayList<>(upstreams);
    }

    /**
     * @return Fallback DNS upstreams settings list.
     */
    public List<UpstreamSettings> getFallbacks() {
        return fallbacks;
    }

    /**
     * @param fallbacks Fallback DNS upstreams settings list.
     */
    public void setFallbacks(List<UpstreamSettings> fallbacks) {
        this.fallbacks = new ArrayList<>(fallbacks);
    }

    /**
     * @return the fallback domains
     */
    public List<String> getFallbackDomains() {
        return fallbackDomains;
    }

    /**
     * @param fallbackDomains Requests for these domains will be forwarded directly to the
     *                        fallback upstreams, if there are any. A wildcard character, `*`,
     *                        which stands for any number of characters, is allowed to appear
     *                        multiple times anywhere except at the end of the domain.
     */
    public void setFallbackDomains(List<String> fallbackDomains) {
        this.fallbackDomains = new ArrayList<>(fallbackDomains);
    }

    /**
     * @return whether search domains detection is enabled
     */
    public boolean isDetectSearchDomains() {
        return detectSearchDomains;
    }

    /**
     * @param detectSearchDomains if true, DNS search domains will be detected
     *                            and appended to the fallback filter automatically
     */
    public void setDetectSearchDomains(boolean detectSearchDomains) {
        this.detectSearchDomains = detectSearchDomains;
    }

    /**
     * @return DNS64 settings. If {@code null}, DNS64 is disabled.
     */
    public Dns64Settings getDns64() {
        return dns64;
    }

    /**
     * @param dns64 DNS64 settings. If {@code null}, DNS64 is disabled.
     */
    public void setDns64(Dns64Settings dns64) {
        this.dns64 = dns64;
    }

    /**
     * @return TTL of the record for the blocked domains (in seconds).
     */
    public long getBlockedResponseTtlSecs() {
        return blockedResponseTtlSecs;
    }

    /**
     * @param blockedResponseTtlSecs TTL of the record for the blocked domains (in seconds).
     */
    public void setBlockedResponseTtlSecs(long blockedResponseTtlSecs) {
        this.blockedResponseTtlSecs = blockedResponseTtlSecs;
    }

    /**
     * @return Filter engine parameters.
     */
    public List<FilterParams> getFilterParams() {
        return filterParams;
    }

    /**
     * @param filterParams Filter engine parameters.
     */
    public void setFilterParams(List<FilterParams> filterParams) {
        this.filterParams = new ArrayList<>(filterParams);
    }

    /**
     * @param filterParams Filter id -> filter data.
     * @param inMemory     If true, data is rules, otherwise data is path to file with rules.
     */
    public void setFilterParams(Map<Integer, String> filterParams, boolean inMemory) {
        this.filterParams = new ArrayList<>(filterParams.size());
        for (final Map.Entry<Integer, String> e : filterParams.entrySet()) {
            getFilterParams().add(new FilterParams(e.getKey(), e.getValue(), inMemory));
        }
    }

    /**
     * @return List of addresses/ports/protocols/etc... to listen on.
     */
    public List<ListenerSettings> getListeners() {
        return listeners;
    }

    /**
     * @param listeners List of addresses/ports/protocols/etc... to listen on.
     */
    public void setListeners(List<ListenerSettings> listeners) {
        this.listeners = new ArrayList<>(listeners);
    }

    /**
     * @return Outbound proxy settings.
     */
    public OutboundProxySettings getOutboundProxy() {
        return outboundProxy;
    }

    /**
     * @param outboundProxy Outbound proxy settings.
     */
    public void setOutboundProxy(OutboundProxySettings outboundProxy) {
        this.outboundProxy = outboundProxy;
    }

    /**
     * @return whether bootstrappers will fetch AAAA records.
     */
    public boolean isIpv6Available() {
        return ipv6Available;
    }

    /**
     * @param ipv6Available if {code false}, bootstrappers will only fetch A records.
     */
    public void setIpv6Available(boolean ipv6Available) {
        this.ipv6Available = ipv6Available;
    }

    /**
     * @return whether the proxy will block AAAA requests.
     */
    public boolean isBlockIpv6() {
        return blockIpv6;
    }

    /**
     * @param blockIpv6 if {@code true}, the proxy will block AAAA requests.
     */
    public void setBlockIpv6(boolean blockIpv6) {
        this.blockIpv6 = blockIpv6;
    }

    /**
     * @return whether optimistic cache is enabled
     */
    public boolean isOptimisticCache() {
        return optimisticCache;
    }

    /**
     * @param optimisticCache enable optimistic cache
     */
    public void setOptimisticCache(boolean optimisticCache) {
        this.optimisticCache = optimisticCache;
    }

    /**
     * @return Enabled log extending for responses which processed with DNSSEC or not
     */
    public boolean isEnabledDNSSECOK() {
        return enableDNSSECOK;
    }

    /**
     * @param enableDNSSECOK Enable DNSSEC OK extension.
     *                       This options tells server that we want to receive DNSSEC records along with normal queries.
     *                       If they exist, request processed event will have DNSSEC flag on.
     *                       WARNING: may increase data usage and probability of TCP fallbacks.
     */
    public void enableDNSSECOK(boolean enableDNSSECOK) {
        this.enableDNSSECOK = enableDNSSECOK;
    }

    /**
     * @return whether retransmission handling is enabled.
     */
    public boolean isEnableRetransmissionHandling() {
        return enableRetransmissionHandling;
    }

    /**
     * @param enableRetransmissionHandling if true, retransmitted requests will be handled
     *                                     using the fallback upstreams only.
     */
    public void setEnableRetransmissionHandling(boolean enableRetransmissionHandling) {
        this.enableRetransmissionHandling = enableRetransmissionHandling;
    }

    /**
     * @return whether stripping of Encrypted Client Hello parameters is enabled.
     */
    public boolean isBlockEch() {
        return blockEch;
    }

    /**
     * @param blockEch if true, Encrypted Client Hello parameters will be striped from responses.
     */
    public void setBlockEch(boolean blockEch) {
        this.blockEch = blockEch;
    }

    /**
     * @return whether parallel upstream queriying is enabled.
     */
    public boolean isEnableParallelUpstreamQueries() {
        return enableParallelUpstreamQueries;
    }

    /**
     * @param enableParallelUpstreamQueries whether to enable parallel upstream querying.
     */
    public void setEnableParallelUpstreamQueries(boolean enableParallelUpstreamQueries) {
        this.enableParallelUpstreamQueries = enableParallelUpstreamQueries;
    }

    /**
     * @return whether switching to fallback upstreams on normal upstreams failure is enabled.
     */
    public boolean isEnableFallbackOnUpstreamsFailure() {
        return enableFallbackOnUpstreamsFailure;
    }

    /**
     * @param enableFallbackOnUpstreamsFailure whether to switch to fallback upstreams when normal upstreams fail
     *                                         to provide a response.
     */
    public void setEnableFallbackOnUpstreamsFailure(boolean enableFallbackOnUpstreamsFailure) {
        this.enableFallbackOnUpstreamsFailure = enableFallbackOnUpstreamsFailure;
    }

    /**
     * @return whether generating a SERVFAIL response on upstreams failure is enabled.
     */
    public boolean isEnableServfailOnUpstreamsFailure() {
        return enableServfailOnUpstreamsFailure;
    }

    /**
     * @param enableServfailOnUpstreamsFailure whether to enable generating a SERVFAIL response when all upstreams
     *                                         (including fallback) fail to provide a response. If {@code false},
     *                                         no response will be sent to the client if no upstreams could provide
     *                                         a response.
     */
    public void setEnableServfailOnUpstreamsFailure(boolean enableServfailOnUpstreamsFailure) {
        this.enableServfailOnUpstreamsFailure = enableServfailOnUpstreamsFailure;
    }

    /**
     * @return Whether HTTP/3 support is enabled DNS-over-HTTPS upstreams.
     */
    public boolean isEnableHttp3() {
        return enableHttp3;
    }

    /**
     * @param enableHttp3 Enable HTTP/3 for DNS-over-HTTPS upstreams if it's able to connect quicker.
     */
    public void setEnableHttp3(boolean enableHttp3) {
        this.enableHttp3 = enableHttp3;
    }

    /**
     * @return Maximum amount of time, in milliseconds, allowed for upstream exchange.
     */
    public long getUpstreamTimeoutMs() {
        return upstreamTimeoutMs;
    }

    /**
     * @param upstreamTimeoutMs Maximum amount of time, in milliseconds, allowed for upstream exchange.
     */
    public void setUpstreamTimeoutMs(long upstreamTimeoutMs) {
        this.upstreamTimeoutMs = upstreamTimeoutMs;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DnsProxySettings that = (DnsProxySettings) o;
        return blockedResponseTtlSecs == that.blockedResponseTtlSecs &&
                ipv6Available == that.ipv6Available &&
                blockIpv6 == that.blockIpv6 &&
                Objects.equals(upstreams, that.upstreams) &&
                Objects.equals(fallbacks, that.fallbacks) &&
                Objects.equals(fallbackDomains, that.fallbackDomains) &&
                detectSearchDomains == that.detectSearchDomains &&
                Objects.equals(dns64, that.dns64) &&
                Objects.equals(filterParams, that.filterParams) &&
                Objects.equals(listeners, that.listeners) &&
                Objects.equals(outboundProxy, that.outboundProxy) &&
                adblockRulesBlockingMode == that.adblockRulesBlockingMode &&
                hostsRulesBlockingMode == that.hostsRulesBlockingMode &&
                Objects.equals(customBlockingIpv4, that.customBlockingIpv4) &&
                Objects.equals(customBlockingIpv6, that.customBlockingIpv6) &&
                dnsCacheSize == that.dnsCacheSize &&
                optimisticCache == that.optimisticCache &&
                enableDNSSECOK == that.enableDNSSECOK &&
                enableRetransmissionHandling == that.enableRetransmissionHandling &&
                blockEch == that.blockEch &&
                enableParallelUpstreamQueries == that.enableParallelUpstreamQueries &&
                enableFallbackOnUpstreamsFailure == that.enableFallbackOnUpstreamsFailure &&
                enableServfailOnUpstreamsFailure == that.enableServfailOnUpstreamsFailure &&
                upstreamTimeoutMs == that.upstreamTimeoutMs;
    }

    @Override
    public int hashCode() {
        return Objects.hash(upstreams, fallbacks, fallbackDomains, detectSearchDomains, dns64, blockedResponseTtlSecs,
                filterParams, listeners, outboundProxy, ipv6Available, blockIpv6, adblockRulesBlockingMode, hostsRulesBlockingMode,
                customBlockingIpv4, customBlockingIpv6,
                dnsCacheSize, optimisticCache, enableDNSSECOK, enableRetransmissionHandling, blockEch,
                enableParallelUpstreamQueries, enableFallbackOnUpstreamsFailure, enableServfailOnUpstreamsFailure,
                upstreamTimeoutMs);
    }

    /**
     * @return the default DNS proxy settings.
     */
    public static DnsProxySettings getDefault() {
        return DnsProxy.getDefaultSettings();
    }
}
