package ru.kirillius.pf.sdn;

import lombok.Getter;
import lombok.SneakyThrows;
import ru.kirillius.pf.sdn.External.API.Components.FRR;
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
import ru.kirillius.pf.sdn.External.API.Components.TDNS;
import ru.kirillius.pf.sdn.External.API.GitSubscription;
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
import ru.kirillius.pf.sdn.External.API.RIPEInfoProvider;
import ru.kirillius.pf.sdn.core.*;
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Auth.TokenService;
import ru.kirillius.pf.sdn.core.Networking.BGPInfoService;
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
import ru.kirillius.pf.sdn.core.Networking.ResolverService;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.core.Util.Wait;
import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static ru.kirillius.pf.sdn.core.Util.CommandLineUtils.getArgument;

/**
 * Entry point for the SDN control application that wires configuration, services, and shutdown handling.
 */
public class App implements Context, Closeable {
    protected final static String CTX = App.class.getSimpleName();

    static {
        SystemLogger.initializeLogging(Level.INFO, List.of(InMemoryLogHandler.class));
    }

    private final AtomicBoolean shouldRestart = new AtomicBoolean(false);
    private final AtomicBoolean running = new AtomicBoolean(true);
    @Getter
    private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
    @Getter
    private final ServiceManager serviceManager;
    @Getter
    private final LauncherConfig launcherConfig;
    @Getter
    private final Config config;

    /**
     * Loads configuration from disk, creating a default file if missing.
     */
    private Config loadConfig() {
        Config loadedConfig = null;
        try {
            loadedConfig = Config.load(launcherConfig.getConfigFile());
        } catch (IOException e) {
            loadedConfig = new Config();
            loadedConfig.setSubscriptions(new ArrayList<>(List.of(
                    new RepositoryConfig("updates", GitSubscription.class, "https://git.kirillius.ru/kirillius/protected-resources-list.git", ""),
                    new RepositoryConfig("local", LocalFilesystemSubscription.class, "/etc/pfsdn.res.d", "")
            )));
            try {
                Config.store(loadedConfig, launcherConfig.getConfigFile());
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
        return loadedConfig;
    }

    /**
     * Instantiates all application services and performs initial wiring.
     */
    private ServiceManager loadServiceManager() {
        var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class, ResolverService.class));
        var infoService = manager.getService(BGPInfoService.class);
        infoService.addProvider(new HEInfoProvider());
        infoService.addProvider(new RIPEInfoProvider());

        return manager;
    }

    /**
     * Ensures an admin password exists, defaulting to {@code admin} when missing.
     */
    private void checkDefaultPassword() {
        if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) {
            SystemLogger.error("There is no password for admin. Setting default password: admin", CTX);
            getServiceManager().getService(AuthManager.class).updatePassword("admin");
        }
    }

    /**
     * Constructs the application binding to the provided launcher configuration.
     */
    @SneakyThrows
    public App(LauncherConfig launcherConfig) {
        this.launcherConfig = launcherConfig;
        config = loadConfig();
        if (config.isDisplayDebuggingInfo()) {
            SystemLogger.setExceptionDumping(true);
        }

        serviceManager = loadServiceManager();
        serviceManager.getService(SubscriptionService.class).triggerUpdate();
        checkDefaultPassword();

        serviceManager.getService(ComponentHandlerService.class).syncComponentsWithConfig();
        serviceManager.getService(ResourceUpdateService.class).start();

        removeObsoleteVersions();
    }

    private void removeObsoleteVersions() {
        var appVersion = serviceManager.getService(AppUpdateService.class).getAppVersion();
        var listedFiles = launcherConfig.getAppLibrary().listFiles();
        if (listedFiles == null) {
            SystemLogger.error("Failed to list files in library path", CTX);
            return;
        }

        var versions = Arrays.stream(listedFiles)
                .filter(file -> file.getName().endsWith(AppUpdateService.EXTENSION))
                .collect(Collectors.toMap(file -> file.getName().replace(Pattern.quote(AppUpdateService.EXTENSION), ""), file -> file));

        if (!versions.containsKey(appVersion + AppUpdateService.EXTENSION)) {
            SystemLogger.error("Unable to remove obsolete version because current version file is not found!", CTX);
            return;
        }

        versions.forEach((key, file) -> {
            if (key.equals(appVersion + AppUpdateService.EXTENSION)) {
                return;
            }
            try {
                SystemLogger.message("Removing obsolete version " + key, CTX);
                if (!file.delete()) {
                    throw new RuntimeException("Delete failed");
                }
            } catch (Exception ex) {
                SystemLogger.error("Failed to delete file " + file.getName(), CTX, ex);
            }
        });
    }

    /**
     * Application entry point.
     */
    public static void main(String[] args) {
        try (var app = new App(LauncherConfig.builder()
                .configFile(new File(getArgument("c", args)))
                .appLibrary(new File(getArgument("l", args)))
                .repository(getArgument("r", args))
                .availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class)).build())) {
            Wait.when(app.running::get);
            if (app.shouldRestart.get()) {
                System.exit(42);
            } else {
                System.exit(0);
            }
        } catch (Exception e) {
            SystemLogger.error("Unhandled error", CTX, e);
            System.exit(1);
        }
    }

    /**
     * Requests the application to exit, optionally restarting.
     */
    public void requestExit(boolean restart) {
        running.set(false);
        shouldRestart.set(restart);
    }

    /**
     * Closes all managed services.
     */
    @Override
    public void close() throws IOException {
        serviceManager.close();
    }
}