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

import lombok.*;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
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.utils.logging.SystemLogger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * Component that synchronises FRR routing instances with the aggregated subnet list.
 */
public final class FRR extends AbstractComponent<FRR.FRRConfig> {
    private final static String SUBNET_PATTERN = "{%subnet}";
    private final static String GW_PATTERN = "{%gateway}";
    private final static String CTX = FRR.class.getSimpleName();
    private final EventListener<NetworkResourceBundle> subscription;

    /**
     * Binds the component to the application context and subscribes to network updates.
     */
    public FRR(Context context) {
        super(context);
        subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
    }

    /**
     * Synchronises FRR instances with the provided subnet list.
     */
    private void updateSubnets(List<IPv4Subnet> subnets) {
        for (var entry : config.instances) {
            SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
            try (var shell = new ShellExecutor(entry.shellConfig)) {
                SystemLogger.message("Fetching existing subnets...", CTX);
                var existingConfig = executeVTYCommand(new String[]{"show running"}, shell);

                var patternOffset = entry.subnetPattern.indexOf(SUBNET_PATTERN);
                if (patternOffset == -1) {
                    SystemLogger.error("Unable to parse FRR config '" + entry.subnetPattern + "' because " + SUBNET_PATTERN + " is not found", CTX);
                    continue;
                }
                var startPattern = entry.subnetPattern.substring(0, patternOffset);
                var endPattern = entry.subnetPattern.substring(patternOffset + SUBNET_PATTERN.length()).replaceAll(Pattern.quote(GW_PATTERN), entry.gateway);


                var existingSubnets = new ArrayList<IPv4Subnet>();
                for (var line : existingConfig.split(Pattern.quote("\n"))) {
                    line = line.trim();
                    if (line.startsWith(startPattern)) {
                        var subnetString = line.substring(startPattern.length()).replaceAll(Pattern.quote(endPattern), "").trim();

                        var subnet = new IPv4Subnet(subnetString);
                        existingSubnets.add(subnet);
                    }
                }

                SystemLogger.message("There is " + existingSubnets.size() + " managed subnets", CTX);


                //удаляем лишние подсети
                var commandsToRemove = existingSubnets.stream()
                        .filter(subnet -> !subnets.contains(subnet) && !entry.essentialSubnets.contains(subnet))
                        .map(subnet -> "no " + entry.subnetPattern
                                .replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
                                .replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
                        .toList();

                if (!commandsToRemove.isEmpty()) {
                    SystemLogger.message( commandsToRemove.size() + " subnets should be removed", CTX);
                    executeVTYCommandBundle(commandsToRemove, true, shell, count -> SystemLogger.message(count + "/" + commandsToRemove.size() + " subnets has been removed", CTX));
                }


                //добавляем новые подсети
                var commandsToAdd = subnets.stream().filter(subnet -> !existingSubnets.contains(subnet))
                        .map(subnet -> entry.subnetPattern
                                .replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
                                .replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
                        .toList();
                if (!commandsToAdd.isEmpty()) {
                    SystemLogger.message( commandsToAdd.size() + " subnets should be added", CTX);
                    executeVTYCommandBundle(commandsToAdd, true, shell, count -> SystemLogger.message(count + "/" + commandsToAdd.size() + " subnets has been added", CTX));
                }


                SystemLogger.message("FRR update is complete", CTX);

            } catch (IOException e) {
                SystemLogger.error("Shell error", CTX, e);
            }

        }
    }

    /**
     * Executes a batch of VTYSH commands optionally wrapping them in configuration mode.
     */
    private void executeVTYCommandBundle(List<String> commands, boolean configMode, ShellExecutor shell, Consumer<Integer> progressCallback) {

        var buffer = new ArrayList<String>();
        if (configMode) {
            buffer.add("conf t");
        }
        for (var i = 0; i < commands.size(); i++) {
            buffer.add(commands.get(i));
            if (i % 100 == 0 && i > 0) {
                executeVTYCommand(buffer.toArray(new String[0]), shell);
                if (progressCallback != null) {
                    progressCallback.accept(i);
                }
                buffer.clear();
                if (configMode) {
                    buffer.add("conf t");
                }
            }
        }

        if (!buffer.isEmpty()) {
            executeVTYCommand(buffer.toArray(new String[0]), shell);
            if (progressCallback != null) {
                progressCallback.accept(commands.size());
            }
        }
    }

    /**
     * Executes a single VTYSH command and returns its stdout.
     */
    private String executeVTYCommand(String[] command, ShellExecutor shell) {
        var buffer = new ArrayList<String>();
        buffer.add("vtysh");
        for (var part : command) {
            buffer.add("-c");
            buffer.add(part);
        }

        return shell.executeCommand(buffer.toArray(new String[0]));
    }

    /**
     * Removes the subscription from the context event handler.
     */
    @Override
    public void close() throws IOException {
        context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
    }

    /**
     * Configuration describing FRR instances and how subnets should be rendered for them.
     */
    @JSONSerializable
    public static class FRRConfig {

        @Getter
        @Setter
        @JSONArrayProperty(type = Entry.class)
        private List<Entry> instances = new ArrayList<>();

        /**
         * Declarative description of a single FRR instance managed by the component.
         */
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @JSONSerializable
        public static class Entry {
            @Getter
            @Setter
            @JSONProperty
            private ShellExecutor.Config shellConfig = new ShellExecutor.Config();

            @Getter
            @Setter
            @JSONProperty
            private String subnetPattern = "ip route {%subnet} {%gateway} 100";

            @Getter
            @Setter
            @JSONProperty
            private String gateway = "127.0.0.1";

            @Getter
            @Setter
            @JSONArrayProperty(type = IPv4Subnet.class,serializer = IPv4Subnet.Serializer.class)
            private List<IPv4Subnet> essentialSubnets = new ArrayList<>();
        }
    }
}
