package io.xpipe.ext.system.incus;

import io.xpipe.app.ext.NetworkContainerStoreState;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.JacksonMapper;
import io.xpipe.ext.base.identity.IdentityValue;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.NonNull;
import lombok.Value;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class IncusCommandView extends CommandViewBase {

    public IncusCommandView(ShellControl shellControl) {
        super(shellControl);
    }

    public Project project(String project) {
        return new Project(project);
    }

    public class Project extends CommandView {

        private final String project;

        public Project(String project) {
            this.project = project;
        }

        @Override
        protected CommandControl build(Consumer<CommandBuilder> builder) {
            var cmd = CommandBuilder.of().add("incus").add("--project", project != null ? project : "default");
            builder.accept(cmd);
            return shellControl
                    .command(cmd)
                    .withErrorFormatter(IncusCommandView::formatErrorMessage)
                    .withExceptionConverter(IncusCommandView::convertException)
                    .elevated(requiresElevation());
        }

        @Override
        protected ShellControl getShellControl() {
            return IncusCommandView.this.getShellControl();
        }

        @Override
        public Project start() throws Exception {
            shellControl.start();
            return this;
        }

        public void start(String containerName) throws Exception {
            build(commandBuilder -> commandBuilder.add("start").addQuoted(containerName))
                    .execute();
        }

        public void stop(String containerName) throws Exception {
            build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
                    .execute();
        }

        public void pause(String containerName) throws Exception {
            build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
                    .execute();
        }

        public CommandControl console(String containerName) {
            return build(commandBuilder -> commandBuilder.add("console").addQuoted(containerName));
        }

        public CommandControl configEdit(String containerName) {
            return build(commandBuilder -> commandBuilder.add("config", "edit").addQuoted(containerName));
        }

        public Optional<ContainerEntry> queryContainerState(String containerName) throws Exception {
            var l = listContainers();
            var found = l.stream()
                    .filter(containerEntry -> (containerEntry.getProject().equals(project)
                                    || project == null
                                            && containerEntry.getProject().equals("default"))
                            && containerEntry.getName().equals(containerName))
                    .findFirst();
            return found;
        }

        public ShellControl exec(String container, String user, Supplier<Boolean> busybox) {
            var sub = shellControl.subShell();
            sub.setDumbOpen(createOpenFunction(container, user, false, busybox));
            sub.setTerminalOpen(createOpenFunction(container, user, true, busybox));
            return sub.withExceptionConverter(IncusCommandView::convertException)
                    .elevated(requiresElevation());
        }

        private ShellOpenFunction createOpenFunction(
                String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {
            return new ShellOpenFunction() {
                @Override
                public CommandBuilder prepareWithoutInitCommand() {
                    var b = execCommand(containerName, terminal).add("su", "-l");
                    if (user != null) {
                        b.addQuoted(user);
                    }
                    return b;
                }

                @Override
                public CommandBuilder prepareWithInitCommand(@NonNull String command) {
                    var b = execCommand(containerName, terminal).add("su", "-l");
                    if (user != null) {
                        b.addQuoted(user);
                    }
                    return b.add(sc -> {
                                var suType = busybox.get();
                                if (suType) {
                                    return "-c";
                                } else {
                                    return "--session-command";
                                }
                            })
                            .addLiteral(command);
                }
            };
        }

        private CommandBuilder execCommand(String containerName, boolean terminal) {
            var c = CommandBuilder.of()
                    .add("incus")
                    .add("--project", project != null ? project : "default")
                    .add("exec", terminal ? "-t" : "-T");
            return c.addQuoted(containerName).add("--");
        }
    }

    private static ElevationFunction requiresElevation() {
        return ElevationFunction.cached("incusRequiresElevation", new ElevationFunction() {
            @Override
            public String getPrefix() {
                return "Incus";
            }

            @Override
            public boolean isSpecified() {
                return true;
            }

            @Override
            public boolean apply(ShellControl shellControl) throws Exception {
                // This is not perfect as it does not respect custom locations for the Incus socket
                // Sadly the socket location changes based on the installation type, and we can't dynamically query the
                // path
                return !shellControl
                        .command("test -S /var/lib/incus/unix.socket && test -w /var/lib/incus/unix.socket || "
                                + "test -S /var/snap/incus/common/incus/unix.socket && test -w /var/snap/incus/common/incus/unix.socket || "
                                + "test -S /var/snap/incus/common/incus/unix.socket.user && test -w /var/snap/incus/common/incus/unix.socket.user || "
                                + "test -S /var/lib/incus/unix.socket.user && test -w /var/lib/incus/unix.socket.user")
                        .executeAndCheck();
            }
        });
    }

    private static String formatErrorMessage(String s) {
        return s;
    }

    private static <T extends Throwable> T convertException(T s) {
        return ErrorEventFactory.expectedIfContains(s);
    }

    @Override
    protected CommandControl build(Consumer<CommandBuilder> builder) {
        var cmd = CommandBuilder.of().add("incus");
        builder.accept(cmd);
        return shellControl
                .command(cmd)
                .withErrorFormatter(IncusCommandView::formatErrorMessage)
                .withExceptionConverter(IncusCommandView::convertException)
                .elevated(requiresElevation());
    }

    @Override
    public IncusCommandView start() throws Exception {
        shellControl.start();
        return this;
    }

    public boolean isSupported() throws Exception {
        return shellControl
                .command("incus --help")
                .withErrorFormatter(IncusCommandView::formatErrorMessage)
                .withExceptionConverter(IncusCommandView::convertException)
                .executeAndCheck();
    }

    public String version() throws Exception {
        return build(commandBuilder -> commandBuilder.add("version")).readStdoutOrThrow();
    }

    public List<DataStoreEntryRef<IncusContainerStore>> listContainers(DataStoreEntryRef<IncusInstallStore> store)
            throws Exception {
        return listContainers().stream()
                .map(s -> {
                    boolean running = s.getState().toLowerCase(Locale.ROOT).equals("running");
                    var c = new IncusContainerStore(
                            store, s.getProject(), s.getName(), IdentityValue.ofBreakout(store.get()));
                    var entry = DataStoreEntry.createNew(c.getContainerName(), c);
                    entry.setStorePersistentState(NetworkContainerStoreState.builder()
                            .containerState(s.getState())
                            .running(running)
                            .ipv4(s.getIpv4Address())
                            .ipv6(s.getIpv6Address())
                            .build());
                    return Optional.of(entry.<IncusContainerStore>ref());
                })
                .flatMap(Optional::stream)
                .toList();
    }

    @Value
    public static class ContainerEntry {
        String project;
        String name;
        String state;
        String ipv4Address;
        String ipv6Address;
    }

    private List<ContainerEntry> listContainers() throws Exception {
        try (var c = build(commandBuilder -> commandBuilder.add("list", "--all-projects", "-f", "json"))
                .start()) {
            var output = c.readStdoutOrThrow();
            var json = JacksonMapper.getDefault().readTree(output);
            var l = new ArrayList<ContainerEntry>();
            for (JsonNode jsonNode : json) {
                var status = jsonNode.required("status").textValue();
                var project = jsonNode.required("project").textValue();
                var name = jsonNode.required("name").textValue();
                var state = jsonNode.required("state");
                var network = state.get("network");
                String ipv4 = null;
                String ipv6 = null;
                if (network != null) {
                    var eth0 = network.get("eth0");
                    if (eth0 == null && network.size() > 0) {
                        for (var it = network.fieldNames(); it.hasNext(); ) {
                            var field = it.next();
                            if (!field.equals("lo")) {
                                eth0 = network.required(field);
                                break;
                            }
                        }
                    }

                    if (eth0 != null) {
                        var addresses = eth0.required("addresses");
                        for (JsonNode address : addresses) {
                            if (!address.required("scope").textValue().equals("global")) {
                                continue;
                            }

                            var family = address.required("family").textValue();
                            if (family.equals("inet")) {
                                ipv4 = address.required("address").textValue();
                            } else if (family.equals("inet6")) {
                                ipv6 = address.required("address").textValue();
                            }
                        }
                    }
                }
                l.add(new ContainerEntry(project, name, status, ipv4, ipv6));
            }
            return l;
        } catch (ProcessOutputException ex) {
            if (ex.getOutput().contains("Error: unknown flag: --all-projects")) {
                return List.of();
            } else {
                throw ex;
            }
        }
    }
}
