package ru.kirillius.pf.sdn.External.API.Components;

import lombok.Getter;
import lombok.Setter;
import org.json.JSONArray;
import org.json.JSONObject;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.External.API.ShellExecutor;
import ru.kirillius.pf.sdn.core.AbstractComponent;
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.Networking.NetworkingService;
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Component integrating with OpenVPN to expose management RPC and synchronize route exports.
 */
public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
    private final static String CTX = OVPN.class.getSimpleName();
    private final EventListener<NetworkResourceBundle> updateEvent;

    private final List<IPv4Subnet> lastSubnets = new ArrayList<>();

    /**
     * Registers the component with the JSON-RPC servlet or defers until it becomes available.
     */
    public OVPN(Context context) {
        super(context);
        var eventsHandler = context.getEventsHandler();
        if (config.restartOnUpdate) {
            updateEvent = eventsHandler.getNetworkManagerUpdateEvent().add(bundle -> {
                var subnets = new HashSet<>(bundle.getSubnets());
                synchronized (lastSubnets) {
                    if (lastSubnets.size() != subnets.size() || !subnets.containsAll(lastSubnets)) {
                        restartSystemService();
                        lastSubnets.clear();
                        lastSubnets.addAll(subnets);
                    }
                }
            });
        } else {
            updateEvent = null;
        }


        var RPC = context.getServiceManager().getService(WebService.class).getJSONRPC();
        if (RPC != null) {
            RPC.addTargetInstance(OVPN.class, this);

        }
    }

    /**
     * Executes the configured command to restart the OpenVPN service.
     */
    @JRPCMethod
    @ProtectedMethod
    public String restartSystemService() {
        SystemLogger.message("Restarting OVPN service", CTX);
        try (var shell = new ShellExecutor(config.shellConfig)) {
            return shell.executeCommand(config.restartCommand.split(Pattern.quote(" ")));
        } catch (IOException e) {
            SystemLogger.error("Error when trying to restart OVPN", CTX, e);
            return e.toString();
        }
    }

    /**
     * Returns a JSON array describing managed routes for OpenVPN clients.
     */
    @JRPCMethod
    @ProtectedMethod
    public JSONArray getManagedRoutes() {
        var array = new JSONArray();
        context.getServiceManager().getService(NetworkingService.class).getOutputResources().getSubnets().stream().map(subnet -> {
            var json = new JSONObject();
            json.put("address", subnet.getAddress());
            json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength())));
            return json;
        }).forEach(array::put);
        return array;
    }


    /**
     * Unregisters RPC listeners when the component is closed.
     */
    @Override
    public void close() {
        var eventsHandler = context.getEventsHandler();
        if (updateEvent != null) {
            eventsHandler.getNetworkManagerUpdateEvent().remove(updateEvent);
        }
    }

    /**
     * Configuration for OpenVPN shell interaction and service restart command.
     */
    @JSONSerializable
    public static class OVPNConfig {
        @Getter
        @Setter
        @JSONProperty
        private volatile ShellExecutor.Config shellConfig = new ShellExecutor.Config();

        @Getter
        @Setter
        @JSONProperty
        private volatile String restartCommand = "rc-service openvpn restart";

        @Getter
        @Setter
        @JSONProperty
        private volatile boolean restartOnUpdate = true;
    }
}
