package io.xpipe.app.cred;

import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.secret.SecretStrategyChoiceConfig;
import io.xpipe.app.storage.ContextualFileReference;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.Validators;
import io.xpipe.app.util.InPlaceSecretValue;
import io.xpipe.app.util.KeyValue;
import io.xpipe.app.util.OsType;

import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;

import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;

import java.util.List;

@Value
@Jacksonized
@Builder
@JsonTypeName("file")
@AllArgsConstructor
public class KeyFileStrategy implements SshIdentityStrategy {

    @SuppressWarnings("unused")
    public static String getOptionsNameKey() {
        return "keyFile";
    }

    @SuppressWarnings("unused")
    public static OptionsBuilder createOptions(Property<KeyFileStrategy> p, SshIdentityStrategyChoiceConfig config) {
        var keyPath = new SimpleObjectProperty<>(
                p.getValue() != null && p.getValue().getFile() != null
                        ? p.getValue().getFile().toAbsoluteFilePath(null)
                        : null);
        p.addListener((observable, oldValue, newValue) -> {
            if (keyPath.get() != null
                    && newValue != null
                    && !ContextualFileReference.of(keyPath.get()).equals(newValue.getFile())) {
                return;
            }

            keyPath.setValue(
                    newValue != null && newValue.getFile() != null
                            ? newValue.getFile().toAbsoluteFilePath(null)
                            : null);
        });
        var keyPasswordProperty =
                new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
        var publicKey =
                new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPublicKey() : null);

        var sync = ContextualFileReferenceSync.of(
                DataStorage.get().getDataDir().resolve("keys"),
                file -> file.getFileName().toString(),
                config.getPerUserKeyFileCheck());

        var passwordChoice = OptionsChoiceBuilder.builder()
                .allowNull(false)
                .property(keyPasswordProperty)
                .customConfiguration(SecretStrategyChoiceConfig.builder()
                        .allowNone(true)
                        .passwordKey("passphrase")
                        .build())
                .available(SecretRetrievalStrategy.getClasses())
                .build()
                .build();

        var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
            struc.promptTextProperty()
                    .bind(Bindings.createStringBinding(
                            () -> {
                                return "ssh-... ABCDEF.... (" + AppI18n.get("publicKeyGenerateNotice") + ")";
                            },
                            AppI18n.activeLanguage()));
            struc.setEditable(false);
        });
        var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
                    ThreadHelper.runFailableAsync(() -> {
                        var sc = config.getFileSystem() != null
                                ? config.getFileSystem().getValue().getStore().getOrStartSession()
                                : LocalShell.getShell();
                        var path = keyPath.get().resolveTildeHome(sc.view().userHome());
                        if (!sc.view().fileExists(path)) {
                            return;
                        }

                        var pubKeyPath = SshIdentityStrategy.getPublicKeyPath(path);
                        if (sc.view().fileExists(pubKeyPath)) {
                            var contents = sc.view().readTextFile(pubKeyPath).strip();
                            Platform.runLater(() -> {
                                publicKey.set(contents);
                            });
                            return;
                        }

                        var contents = sc.view().readRawFile(path);
                        var generated = ProcessControlProvider.get()
                                .generatePublicSshKey(InPlaceSecretValue.of(contents), keyPasswordProperty.get());
                        if (generated != null) {
                            Platform.runLater(() -> {
                                publicKey.set(generated);
                            });
                        }
                    });
                })
                .describe(d -> d.nameKey("generatePublicKey"))
                .disable(keyPath.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
        var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
                    ClipboardHelper.copyText(publicKey.get());
                })
                .disable(publicKey.isNull())
                .describe(d -> d.nameKey("copyPublicKey"));

        var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
        publicKeyBox.setMainReference(publicKeyField);

        keyPath.addListener((observable, oldValue, newValue) -> {
            if (newValue == null) {
                return;
            }

            ThreadHelper.runFailableAsync(() -> {
                var pubFile = SshIdentityStrategy.getPublicKeyPath(newValue);
                var fs =
                        config.getFileSystem() != null && config.getFileSystem().getValue() != null
                                ? config.getFileSystem().getValue().getStore()
                                : (ShellStore) DataStorage.get().local().getStore();
                var ex = fs.getOrStartSession().view().fileExists(pubFile);
                if (ex) {
                    var contents =
                            fs.getOrStartSession().view().readTextFile(pubFile).strip();
                    Platform.runLater(() -> {
                        publicKey.set(contents);
                    });
                }
            });
        });

        return new OptionsBuilder()
                .name("location")
                .description("locationDescription")
                .addComp(
                        new ContextualFileReferenceChoiceComp(
                                config.getFileSystem() != null
                                        ? config.getFileSystem()
                                        : new ReadOnlyObjectWrapper<>(
                                                DataStorage.get().local().ref()),
                                keyPath,
                                config.isAllowKeyFileSync() ? sync : null,
                                List.of(),
                                e -> {
                                    if (config.getFileSystem() == null) {
                                        return e.equals(DataStorage.get().local());
                                    }

                                    var fs = config.getFileSystem().getValue();
                                    if (fs == null) {
                                        return e.equals(DataStorage.get().local());
                                    } else {
                                        return e.equals(fs.get());
                                    }
                                },
                                false),
                        keyPath)
                .nonNull()
                .nameAndDescription("keyPassphrase")
                .sub(passwordChoice, keyPasswordProperty)
                .nonNull()
                .nameAndDescription("inPlacePublicKey")
                .documentationLink(DocumentationLink.SSH_PUBLIC_KEY)
                .addComp(publicKeyBox, publicKey)
                .bind(
                        () -> {
                            return new KeyFileStrategy(
                                    ContextualFileReference.of(keyPath.get()),
                                    keyPasswordProperty.get(),
                                    publicKey.get());
                        },
                        p);
    }

    ContextualFileReference file;
    SecretRetrievalStrategy password;
    String publicKey;

    public void checkComplete() throws ValidationException {
        Validators.nonNull(file);
        Validators.nonNull(password);
    }

    @Override
    public void prepareParent(ShellControl parent) throws Exception {
        if (file == null) {
            return;
        }

        var s = file.toAbsoluteFilePath(parent);
        // The ~ is supported on all platforms, so manually replace it here for Windows
        if (s.startsWith("~")) {
            s = s.resolveTildeHome(parent.view().userHome());
        }
        var resolved = parent.getShellDialect()
                .evaluateExpression(parent, s.toString())
                .readStdoutOrThrow();
        if (!parent.getShellDialect().createFileExistsCommand(parent, resolved).executeAndCheck()) {
            var systemName = parent.getSourceStore()
                    .flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore, false))
                    .map(e -> DataStorage.get().getStoreEntryDisplayName(e));
            var msg = "Identity file " + resolved + " does not exist"
                    + (systemName.isPresent() ? " on system " + systemName.get() : "");
            throw ErrorEventFactory.expected(new IllegalArgumentException(msg));
        }

        if (resolved.endsWith(".ppk")) {
            var ex = new IllegalArgumentException("Identity file " + resolved
                    + " is in non-standard PuTTY Private Key format (.ppk), which is not supported by OpenSSH. Please export/convert it to a "
                    + "standard format like .pem via PuTTY");
            ErrorEventFactory.preconfigure(
                    ErrorEventFactory.fromThrowable(ex).expected().link("https://www.puttygen.com/convert-pem-to-ppk"));
            throw ex;
        }

        if (resolved.endsWith(".pub")) {
            throw ErrorEventFactory.expected(new IllegalArgumentException("Identity file " + resolved
                    + " is marked to be a public key file, SSH authentication requires the private key"));
        }

        if (parent.getOsType() != OsType.WINDOWS) {
            // Try to preserve the same permission set
            parent.command(CommandBuilder.of()
                            .add("test", "-w")
                            .addFile(resolved)
                            .add("&&", "chmod", "600")
                            .addFile(resolved)
                            .add("||", "chmod", "400")
                            .addFile(resolved))
                    .executeAndCheck();
        }
    }

    @Override
    public void buildCommand(CommandBuilder builder) {}

    @Override
    public List<KeyValue> configOptions(ShellControl sc) throws Exception {
        return List.of(
                KeyValue.raw("IdentitiesOnly", "yes"),
                KeyValue.raw("IdentityAgent", "none"),
                KeyValue.escape(
                        "IdentityFile",
                        file.toAbsoluteFilePath(sc).resolveTildeHome(sc.view().userHome())),
                KeyValue.raw("PKCS11Provider", "none"));
    }

    @Override
    public SecretRetrievalStrategy getAskpassStrategy() {
        return password;
    }

    public PublicKeyStrategy getPublicKeyStrategy() {
        return PublicKeyStrategy.Fixed.of(publicKey);
    }
}
