package be.iminds.ilabt.jfed.ssh_terminal_tool.ssh_key_info;

import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.InvalidSshKeyInfoException;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.PuTTYFilesKeyInfo;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.putty.PuTTYPrivateKeyFile;
import be.iminds.ilabt.jfed.util.JFedUtils;
import be.iminds.ilabt.jfed.util.KeyUtil;
import be.iminds.ilabt.jfed.util.tmp_file_helpers.PermanentFile;
import be.iminds.ilabt.jfed.util.tmp_file_helpers.TmpContentFile;
import be.iminds.ilabt.jfed.util.tmp_file_helpers.TmpFile;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Objects;

/**
 * PreferencesSshKeyInfo
 */
public class PreferencesSshKeyInfo implements PuTTYFilesKeyInfo {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(PreferencesSshKeyInfo.class);

    @Nullable
    private final File privKeyFile;
    @Nullable
    private final File pubKeyFile;
    @Nonnull
    private final String privKeyText;
    @Nullable
    private final String pubKeyText;

    private final RSAPublicKey publicKey;
    private PrivateKey privateKey = null;


    public PreferencesSshKeyInfo(@Nullable File privKeyFile, @Nullable File pubKeyFile,
                                 @Nonnull String privKeyText, @Nullable String pubKeyText) throws InvalidSshKeyInfoException {
        this.privKeyFile = privKeyFile;
        this.pubKeyFile = pubKeyFile;
        this.privKeyText = privKeyText;
        this.pubKeyText = pubKeyText;


        RSAPublicKey publicKey = null;

        if (!KeyUtil.hasEncryptedRsaPrivateKey(privKeyText)) {

            try {
                this.privateKey = KeyUtil.pemToAnyPrivateKey(privKeyText, null);
            } catch (KeyUtil.PEMDecodingException e) {
                LOG.debug("Could not parse private key (privKeyFile=" + privKeyFile + " privKeyText head=" + privKeyText.substring(0, 50) + "). error: " + e.getMessage(), e);
                throw new InvalidSshKeyInfoException("Could not parse private key contents", e);
            }

            //check if public key can be derived from private key
            if (privateKey != null && privateKey instanceof RSAPrivateKey) {
                RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
                publicKey = KeyUtil.rsaPrivateKeyToRsaPublicKey(rsaPrivateKey);
            }
        }

        if (publicKey == null) {
            /*
            We end up here in two occasions:
               - The private key is currently encrypted;
               - The private key is in a format that doesn't allow to extract the public key.

               Either way, we expect a valid source for the public key to create this PreferencesSshKeyInfo
             */
            if (pubKeyText == null)
                throw new InvalidSshKeyInfoException("A public key must be provided when the private key is encrypted");

            try {
                publicKey = KeyUtil.openSshAuthorizedKeysFormatRsaPublicKey(pubKeyText);
            } catch (RuntimeException ex) {
                throw new InvalidSshKeyInfoException("A valid public key must be provided when the private key is encrypted");
            }
        } else if (pubKeyText != null) {
            //check that the derived public key from the private key matches the given public key
            RSAPublicKey providedPublicKey = KeyUtil.openSshAuthorizedKeysFormatRsaPublicKey(pubKeyText);

            if (!Objects.equals(publicKey.getPublicExponent(), providedPublicKey.getPublicExponent())) {
                throw new InvalidSshKeyInfoException("Provided public and private key do not match!");
            }
        }

        assert publicKey != null;
        this.publicKey = publicKey;
    }


    @Override
    public TmpFile getPrivateKeyFile() {
        if (privKeyFile != null) {
            return new PermanentFile(privKeyFile);
        } else {
            File tmpDir = new File(JFedUtils.getUserDataDirectoryFile(), "tmp");
            if (!tmpDir.exists()) //noinspection ResultOfMethodCallIgnored
                tmpDir.mkdir();

            return new TmpContentFile("sskKeyPrefE", "pem", privKeyText, true/*assureUserOnlyPermissions*/);
        }
    }


    @Override
    public TmpFile getPuttyKeyFile() {
        if (publicKey == null || privateKey == null) return null;

        return getUnlockedPuttyKeyFile();
    }

    @Override
    public TmpFile getUnencryptedPrivateKeyFile() {
        if (!KeyUtil.hasEncryptedRsaPrivateKey(privKeyText)) {
            //the original file wasn't locked, so we can return that one
            return getPrivateKeyFile();
        } else if (!isLocked()) {
            //original file was locked, but user unlocked the private key in the meanwhile
            return new TmpContentFile("sskKeyPrefUE", ".pem", new String(KeyUtil.privateKeyToAnyPem(privateKey)), true/*assureUserOnlyPermissions*/);
        } else {
            return null;
        }
    }

    @Override
    public TmpFile getUnlockedPuttyKeyFile() {
        if (publicKey == null || privateKey == null) return null;

        String ppkContent =
                PuTTYPrivateKeyFile.createContent("jFed-tmp-ppk", publicKey, (RSAPrivateCrtKey) privateKey, null);
        return new TmpContentFile("sskKeyPrefUE", ".ppk", ppkContent);
    }

    @Nullable
    @Override
    public String getComment() {
        return null;
    }

    @Override
    public boolean hasUnencryptedPrivateKey() {
        return privateKey != null;
    }

    @Override
    public boolean isLocked() {
        return privateKey == null;
    }

    @Override
    public boolean unlock(char[] password) {
        try {
            this.privateKey = KeyUtil.pemToAnyPrivateKey(privKeyText, password);

            //check if public key can be derived from private key
            if (privateKey != null && privateKey instanceof RSAPrivateKey) {
                RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
                RSAPublicKey derivedPublicKey = KeyUtil.rsaPrivateKeyToRsaPublicKey(rsaPrivateKey);

                if (derivedPublicKey != null &&
                        !Objects.equals(derivedPublicKey.getPublicExponent(), publicKey.getPublicExponent())) {
                    throw new IllegalStateException("The public key derived from the private key doesn't match the public key in this PreferencesSshKeyInfo");
                }
            }

            return true;
        } catch (KeyUtil.PEMDecodingException e) {
            LOG.debug("Could not decode private key with provided password");
            return false;
        }
    }

    @Nonnull
    @Override
    public PublicKey getPublicKey() {
        return publicKey;
    }

    @Override
    public PrivateKey getPrivateKey() {
        return privateKey;
    }
}
