package ru.kirillius.pf.sdn.web.RPC;

import org.json.JSONArray;
import org.json.JSONObject;
import ru.kirillius.json.DefaultPropertySerializer;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.web.ProtectedMethod;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * JSON-RPC handler for subscription lifecycle and resource selection operations.
 */
public class SubscriptionManager implements RPC {
    private final Context context;

    /**
     * Creates the handler bound to the application context.
     */
    public SubscriptionManager(Context context) {
        this.context = context;
    }

    /**
     * Indicates whether the subscription service is updating.
     */
    @JRPCMethod
    @ProtectedMethod
    public boolean isUpdating() {
        return context.getServiceManager().getService(SubscriptionService.class).isUpdatingNow();
    }

    /**
     * Requests the subscription service to refresh repositories.
     */
    @JRPCMethod
    @ProtectedMethod
    public void triggerUpdate() {
        context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
    }

    /**
     * Returns the list of resource identifiers currently subscribed to.
     */
    @JRPCMethod
    @ProtectedMethod
    public JSONArray getSubscribedResources() {
        return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null);
    }

    /**
     * Returns all available resources grouped by repository.
     */
    @JRPCMethod
    @ProtectedMethod
    public JSONObject getAvailableResources() {
        return JSONUtility.serializeMap(context.getServiceManager().getService(SubscriptionService.class).getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
    }

    /**
     * Persists the new selection of subscribed resources and triggers an update.
     */
    @JRPCMethod
    @ProtectedMethod
    public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) {
        context.getConfig().setSubscribedResources(JSONUtility.deserializeCollection(subscribedResources, String.class, null).collect(Collectors.toList()));
        triggerUpdate();
    }

    @JRPCMethod
    @ProtectedMethod
    public void writeLocalResourceFile(
            @JRPCArgument(name = "name") String name,
            @JRPCArgument(name = "description") String description,
            @JRPCArgument(name = "domains") JSONArray domains,
            @JRPCArgument(name = "ASN") JSONArray ASN,
            @JRPCArgument(name = "subnets") JSONArray subnets,
            @JRPCArgument(name = "addresses") JSONArray addresses,
            @JRPCArgument(name = "autoResolve") boolean autoResolve,
            @JRPCArgument(name = "storage") String storage,
            @JRPCArgument(name = "subscribe") boolean subscribe) throws IOException {
        var repositoryConfig = findLocalRepo(storage);
        var directory = new File(repositoryConfig.getSource());
        if (!directory.exists()) {
            throw new FileNotFoundException("Unable to find directory " + directory.getAbsolutePath());
        }

        var domainList = new ArrayList<String>();

        domains.forEach(d -> domainList.add(d.toString()));

        try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(directory, name + ".json"))))) {
            var merged = JSONUtility.deserializeCollection(subnets, IPv4Subnet.class, null).collect(Collectors.toList());
            addresses.forEach(address -> {
                merged.add(new IPv4Subnet(address.toString(), 32));
            });
            //noinspection rawtypes
            @SuppressWarnings("unchecked")
            var asnList = (List<Integer>) JSONUtility.deserializeCollection(ASN, Integer.class, (Class) DefaultPropertySerializer.class).toList();
            writer.write(JSONUtility.serializeStructure(
                    NetworkResourceBundle.builder()
                            .ASN(asnList)
                            .subnets(merged)
                            .domains(domainList)
                            .description(description)
                            .resolveDomains(autoResolve)
                            .build()
            ).toString(2));
        }
        if (subscribe) {
            var subscribedResources = context.getConfig().getSubscribedResources();
            subscribedResources.add(repositoryConfig.getName() + ":" + name);
        }

        context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
    }

    private RepositoryConfig findLocalRepo(String storage) throws FileNotFoundException {
        var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class) && repositoryConfig.getName().equals(storage)).findFirst();
        if (optional.isEmpty()) {
            throw new IllegalStateException("Unable to find local repository with name " + storage);
        }

        return optional.get();
    }

    @JRPCMethod
    @ProtectedMethod
    public void removeLocalResourceFile(
            @JRPCArgument(name = "name") String name,
            @JRPCArgument(name = "storage") String storage) throws IOException {

        var repositoryConfig = findLocalRepo(storage);

        var directory = new File(repositoryConfig.getSource());
        if (!directory.exists()) {
            throw new FileNotFoundException("Unable to find directory " + directory.getAbsolutePath());
        }

        var localFile = new File(directory, name + ".json");
        if (!localFile.delete()) {
            throw new IOException("Unable to delete local file " + localFile.getAbsolutePath());
        }

        var subscribedResources = context.getConfig().getSubscribedResources();
        subscribedResources.remove(repositoryConfig.getName() + ":" + name);

        context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
    }

    @JRPCMethod
    @ProtectedMethod
    public JSONArray getLocalRepositories(
    ) {
        var names = context.getConfig().getSubscriptions().stream()
                .filter(repositoryConfig ->
                        repositoryConfig.getType().equals(LocalFilesystemSubscription.class)
                ).map(RepositoryConfig::getName).toList();

        return JSONUtility.serializeCollection(names, String.class, null);
    }
}
