package ru.kirillius.pf.sdn.core;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.json.JSONObject;
import org.json.JSONTokener;
import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
 * Represents persisted application settings and component configuration bundles.
 */
@NoArgsConstructor
@JSONSerializable
public class Config {
    /**
     * Update subscriptions every N hours
     */
    @Getter
    @Setter
    @JSONProperty
    private volatile int updateSubscriptionsInterval = 6;

    /**
     * Remember downloaded ASN prefixes
     */
    @Getter
    @Setter
    @JSONProperty
    private volatile boolean cachingAS = true;

    @Getter
    @Setter
    @JSONProperty(required = false)
    private volatile boolean cachingDomains = true;

    @Getter
    @Setter
    @JSONArrayProperty(type = String.class, required = false)
    private volatile List<String> domainResolvers = List.of("8.8.8.8", "77.88.8.8");

    /**
     * Time in minutes
     */
    @Getter
    @Setter
    @JSONProperty(required = false)
    private volatile int domainLookupInterval = 5;

//    @Getter
//    @Setter
//    @JSONProperty(required = false)
//    private volatile int autoLookupPrefixLength = 24;

    /**
     * Time in hours
     */
    @Getter
    @Setter
    @JSONProperty(required = false)
    private volatile int domainsTimeToLive = 48;

    /**
     * Update ASN prefixes every N hours
     */
    @Getter
    @Setter
    @JSONProperty
    private volatile int updateASInterval = 12;

    @Getter
    private volatile File loadedConfigFile = null;
    /**
     * Web interface host
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile String host = "0.0.0.0";

    /**
     * Path where to store temporary data
     */

    @Setter
    @Getter
    @JSONProperty
    private volatile File cacheDirectory = new File("/var/cache/pf-sdn");

    /**
     * List of subscription sources
     */
    @Setter
    @Getter
    @JSONArrayProperty(type = RepositoryConfig.class)
    private volatile List<RepositoryConfig> subscriptions = new ArrayList<>();

    /**
     * List of subscribed resources
     */
    @Setter
    @Getter
    @JSONArrayProperty(type = String.class)
    private volatile List<String> subscribedResources = Collections.emptyList();

    /**
     * Config of every component
     */
    @Setter
    @Getter
    @JSONProperty(required = false)
    private volatile ComponentConfigStorage componentsConfig = new ComponentConfigStorage();

    /**
     * List of enabled components
     */
    @Setter
    @Getter
    @JSONArrayProperty(type = Class.class, required = false)
    private volatile List<Class<? extends Component<?>>> enabledComponents = new ArrayList<>();
    /**
     * Special hash salt for password hashing
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile String passwordSalt = UUID.randomUUID().toString();

    /**
     * Password hash
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile String passwordHash = "";
    /**
     * Web service listen port
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile int httpPort = 8081;

    /**
     * Use special algorithm to minify prefix count
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile boolean mergeSubnets = true;
    /**
     * Display debug info in logs
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile boolean displayDebuggingInfo = true;
    /**
     * Merge two subnets into one bigger subnet when new subnet will be used more or equals this percents
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile int mergeSubnetsWithUsage = 51;

    /**
     * Custom routing config
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile NetworkResourceBundle customResources = new NetworkResourceBundle();
    /**
     * Ignore these resources
     */
    @Setter
    @Getter
    @JSONProperty
    private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle();

    public void merge(Config other) {
        if (other == null) {
            return;
        }

        setUpdateSubscriptionsInterval(other.getUpdateSubscriptionsInterval());
        setCachingAS(other.isCachingAS());
        setUpdateASInterval(other.getUpdateASInterval());
        setHost(other.getHost());
        setCacheDirectory(other.getCacheDirectory());
        setSubscriptions(other.getSubscriptions());
        setSubscribedResources(other.getSubscribedResources());
        setEnabledComponents(other.getEnabledComponents());
        setPasswordSalt(other.getPasswordSalt());
        setPasswordHash(other.getPasswordHash());
        setHttpPort(other.getHttpPort());
        setMergeSubnets(other.isMergeSubnets());
        setDisplayDebuggingInfo(other.isDisplayDebuggingInfo());
        setMergeSubnetsWithUsage(other.getMergeSubnetsWithUsage());
        setCustomResources(other.getCustomResources());
        setFilteredResources(other.getFilteredResources());
    }

    /**
     * Persists the given configuration into the supplied file as JSON.
     */
    public static void store(Config config, File file) throws IOException {
        try (var fileInputStream = new FileOutputStream(file)) {
            try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
                var json = serialize(config);
                writer.write(json.toString(4));
                writer.flush();
                config.initialJSON = json;
            }
        }
    }

    /**
     * Serialises the configuration into a JSONObject representation.
     */
    public static JSONObject serialize(Config config) {
        return JSONUtility.serializeStructure(config);
    }

    /**
     * Reconstructs a configuration instance from JSON data.
     */
    public static Config deserialize(JSONObject object) {
        return JSONUtility.deserializeStructure(object, Config.class);
    }

    /**
     * Loads a configuration from disk and tracks the original JSON for modification detection.
     */
    public static Config load(File file) throws IOException {
        try (var stream = new FileInputStream(file)) {
            var json = new JSONObject(new JSONTokener(stream));
            var config = deserialize(json);
            config.loadedConfigFile = file;
            config.initialJSON = json;
            return config;
        }
    }

    private JSONObject initialJSON = new JSONObject();

    /**
     * Indicates whether the in-memory configuration diverges from the initially loaded snapshot.
     */
    public boolean isModified() {
        return !initialJSON.similar(serialize(this));
    }
}
