package org.jivesoftware.smackx.ikey;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smackx.ikey.element.IkeyElement;
import org.jivesoftware.smackx.ikey.element.ProofElement;
import org.jivesoftware.smackx.ikey.element.SignedElement;
import org.jivesoftware.smackx.ikey.element.SubordinateElement;
import org.jivesoftware.smackx.ikey.element.SubordinateListElement;
import org.jivesoftware.smackx.ikey.element.SuperordinateElement;
import org.jivesoftware.smackx.ikey.mechanism.IkeySignatureCreationMechanism;
import org.jivesoftware.smackx.ikey.mechanism.IkeySignatureVerificationMechanism;
import org.jivesoftware.smackx.ikey.provider.IkeyElementProvider;
import org.jivesoftware.smackx.ikey.provider.SubordinateListElementProvider;
import org.jivesoftware.smackx.ikey.record.IkeyRecord;
import org.jivesoftware.smackx.ikey.record.IkeyStore;
import org.jivesoftware.smackx.ikey.record.IkeySubordinateRecord;
import org.jivesoftware.smackx.ikey.record.OxSubordinateRecord;
import org.jivesoftware.smackx.ikey.util.IkeyConstants;
import org.jivesoftware.smackx.ikey.util.UnsupportedSignatureAlgorithmException;
import org.jivesoftware.smackx.ikey_ox.OxIkeySignatureCreationMechanism;
import org.jivesoftware.smackx.ikey_ox.OxIkeySignatureVerificationMechanism;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
import org.jivesoftware.smackx.pep.PepEventListener;
import org.jivesoftware.smackx.pep.PepManager;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.pubsub.PubSubUri;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.crypto.OpenPgpSecretKeyBackupPassphraseGenerator;
import org.mercury_im.messenger.core.crypto.SecureRandomSecretKeyBackupPassphraseGenerator;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.key.protection.SecretKeyRingProtector;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.jivesoftware.smackx.ikey.util.IkeyConstants.SUPERORDINATE_NODE;

public final class IkeyManager extends Manager {

    private static final Logger LOGGER = Logger.getLogger(IkeyManager.class.getName());
    private static final Map<XMPPConnection, IkeyManager> INSTANCES = new WeakHashMap<>();

    private final OpenPgpSecretKeyBackupPassphraseGenerator backupPassphraseGenerator;

    static {
        // TODO: Replace with .providers file once merged into Smack
        ProviderManager.addExtensionProvider(IkeyElement.ELEMENT, IkeyElement.NAMESPACE, new IkeyElementProvider());
        ProviderManager.addExtensionProvider(SubordinateListElement.ELEMENT, SubordinateListElement.NAMESPACE, new SubordinateListElementProvider());
    }

    private IkeyStore store;

    private IkeyManager(XMPPConnection connection) {
        super(connection);
        this.backupPassphraseGenerator = new SecureRandomSecretKeyBackupPassphraseGenerator();
    }

    public static synchronized IkeyManager getInstanceFor(XMPPConnection connection) {
        IkeyManager manager = INSTANCES.get(connection);
        if (manager == null) {
            manager = new IkeyManager(connection);
            INSTANCES.put(connection, manager);
        }
        return manager;
    }

    public SecretkeyElement fetchSecretIdentityKey()
            throws InterruptedException, PubSubException.NotALeafNodeException,
            XMPPException.XMPPErrorException, SmackException.NotConnectedException,
            SmackException.NoResponseException {
        return OpenPgpPubSubUtil.fetchSecretKey(PepManager.getInstanceFor(connection()), SUPERORDINATE_NODE);
    }

    public OpenPgpSecretKeyBackupPassphrase depositIdentityKeyBackup()
            throws PGPException, InterruptedException, SmackException.NoResponseException,
            SmackException.NotConnectedException, SmackException.FeatureNotSupportedException,
            XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException {
        PGPSecretKeyRing secretKeys = store.loadSecretKey();
        OpenPgpSecretKeyBackupPassphrase passphrase = store.loadBackupPassphrase();

        return depositIdentityKeyBackup(secretKeys, passphrase);
    }

    public OpenPgpSecretKeyBackupPassphrase depositIdentityKeyBackup(PGPSecretKeyRing secretKey, OpenPgpSecretKeyBackupPassphrase passphrase)
            throws InterruptedException, SmackException.NoResponseException,
            SmackException.NotConnectedException, SmackException.FeatureNotSupportedException,
            XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException, PGPException {
        SecretkeyElement secretkeyElement = SecretKeyBackupHelper.createSecretkeyElement(secretKey.getEncoded(), passphrase);
        OpenPgpPubSubUtil.depositSecretKey(connection(), secretkeyElement, SUPERORDINATE_NODE);

        return passphrase;
    }

    public IkeyElement createOxIkeyElement(PGPSecretKeyRing secretKeys,
                                           SecretKeyRingProtector keyRingProtector,
                                           SubordinateElement... subordinateElements) throws IOException {
        IkeySignatureCreationMechanism mechanism = new OxIkeySignatureCreationMechanism(secretKeys, keyRingProtector);
        SuperordinateElement superordinateElement = new SuperordinateElement(secretKeys.getPublicKey().getEncoded());
        SubordinateListElement subordinateListElement = new SubordinateListElement(connection().getUser().asEntityBareJid(),
                new Date(), Arrays.asList(subordinateElements));
        return createIkeyElement(mechanism, superordinateElement, subordinateListElement);
    }

    public boolean deleteSecretIdentityKeyNode()
            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
            InterruptedException, SmackException.NoResponseException {
        return OpenPgpPubSubUtil.deleteSecretKeyNode(PepManager.getInstanceFor(connection()), SUPERORDINATE_NODE);
    }

    public void startListeners() {
        PepManager.getInstanceFor(connection())
                .addPepEventListener(IkeyConstants.SUBORDINATES_NODE, IkeyElement.class, ikeyPepEventListener);
    }

    public void stopListeners() {
        PepManager.getInstanceFor(connection())
                .removePepEventListener(ikeyPepEventListener);
    }

    public void setStore(IkeyStore store) {
        this.store = store;
    }

    public IkeyElement createIkeyElement(IkeySignatureCreationMechanism mechanism,
                                         SuperordinateElement superordinateElement,
                                         SubordinateListElement subordinateListElement)
            throws IOException {
        SignedElement signedElement = new SignedElement(subordinateListElement);
        ProofElement proofElement = new ProofElement(mechanism.createSignature(signedElement.getUtf8Bytes()));
        return new IkeyElement(mechanism.getType(), superordinateElement, signedElement, proofElement);
    }

    public void storeAndPublishElement(IkeyElement ikeyElement)
            throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
            SmackException.NotConnectedException, SmackException.NoResponseException, IOException, PGPException {
        IkeyRecord record = elementToRecord(ikeyElement);
        record.setTrust(OpenPgpTrustStore.Trust.trusted);
        store.storeIkeyRecord(connection().getUser().asEntityBareJid(), record);
        publishIkeyElement(ikeyElement);
    }

    public void publishIkeyElement(IkeyElement element)
            throws InterruptedException, PubSubException.NotALeafNodeException,
            XMPPException.XMPPErrorException, SmackException.NotConnectedException,
            SmackException.NoResponseException {
        PepManager.getInstanceFor(connection())
                .publish(IkeyConstants.SUBORDINATES_NODE, new PayloadItem<>(element));
    }

    public IkeyElement fetchIkeyElementOf(EntityBareJid jid)
            throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
            SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException {
        PubSubManager pubSubManager = PubSubManager.getInstanceFor(connection(), jid);
        return fetchIkeyElementFrom(pubSubManager);
    }

    public IkeyElement fetchOwnIkeyElement()
            throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
            SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException {
        PubSubManager pubSubManager = PubSubManager.getInstanceFor(connection());
        return fetchIkeyElementFrom(pubSubManager);
    }

    private static IkeyElement fetchIkeyElementFrom(PubSubManager pubSubManager)
            throws PubSubException.NotALeafNodeException, SmackException.NoResponseException,
            SmackException.NotConnectedException, InterruptedException, XMPPException.XMPPErrorException,
            PubSubException.NotAPubSubNodeException {
        LeafNode node = pubSubManager.getLeafNode(IkeyConstants.SUBORDINATES_NODE);
        List<PayloadItem<IkeyElement>> items = node.getItems(1);
        if (items.isEmpty()) {
            return null;
        } else {
            return items.get(0).getPayload();
        }
    }

    private void processIkeyElement(EntityBareJid from, IkeyElement element)
            throws IOException, UnsupportedSignatureAlgorithmException, PGPException {
        IkeyRecord newRecord = elementToRecord(element);
        if (isFromTheFuture(newRecord)) {
            LOGGER.log(Level.WARNING, "Received ikey element appears to be from the future: " +
                    newRecord.getTimestamp());
            return;
        }

        IkeyRecord existingRecord = store.loadIkeyRecord(newRecord.getJid());

        if (existsSameOrNewerRecord(newRecord, existingRecord)) {
            LOGGER.log(Level.WARNING, "There exists this exact, or a newer ikey record in the database for " + from);
            return;
        }

        if (!verifyIkeyElement(from, element)) {
            LOGGER.log(Level.WARNING, "Invalid signature on ikey element of " + from);
            return;
        }

        if (isContenderElement(newRecord, store.loadIkeyRecord(from))) {
            LOGGER.log(Level.INFO, "Storing contender element for " + from);
            store.storeContenderIkeyRecord(from, newRecord);
        } else {
            if (existingRecord != null) {
                newRecord.setTrust(existingRecord.getTrust());
            }
            PGPSecretKeyRing secretKeys = store.loadSecretKey();
            if (secretKeys != null && new OpenPgpV4Fingerprint(secretKeys).equals(new OpenPgpV4Fingerprint(newRecord.getSuperordinate()))) {
                newRecord.setTrust(OpenPgpTrustStore.Trust.trusted);
            }
            LOGGER.log(Level.INFO, "Storing ikey record " + newRecord);
            store.storeIkeyRecord(from, newRecord);
        }
    }

    private IkeyRecord elementToRecord(IkeyElement element) throws IOException, PGPException {
        SubordinateListElement childElement = element.getSignedElement().getChildElement();

        EntityBareJid jid = childElement.getJid().asEntityBareJidOrThrow();
        Date timestamp = childElement.getTimestamp();

        byte[] pubKeyBytes = element.getSuperordinate().getPubKeyBytes();
        PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(pubKeyBytes);
        OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(publicKeys);

        ByteArrayInputStream signed = new ByteArrayInputStream(element.getSignedElement().getUtf8Bytes());
        DecryptionStream verifier = PGPainless.createDecryptor().onInputStream(signed)
                .doNotDecrypt().verifyDetachedSignature(element.getProof().getSignatureBytes())
                .verifyWith(publicKeys)
                .ignoreMissingPublicKeys()
                .build();

        ByteArrayOutputStream dummyOut = new ByteArrayOutputStream();
        Streams.pipeAll(verifier, dummyOut);
        verifier.close();
        dummyOut.close();

        if (!verifier.getResult().getVerifiedSignatures().containsKey(fingerprint)) {
            throw new PGPException("Signature could not be verified.");
        }

        List<IkeySubordinateRecord> subordinateRecords = new ArrayList<>();
        for (SubordinateElement subordinate : element.getSignedElement().getChildElement().getSubordinates()) {
            if (subordinate.getType().equals(OpenPgpElement.NAMESPACE)) {
                OxSubordinateRecord sr = new OxSubordinateRecord();
                sr.setUri(subordinate.getUri());
                sr.setOxFingerprint(new OpenPgpV4Fingerprint(subordinate.getFingerprint()));
                subordinateRecords.add(sr);
            }
        }
        IkeyRecord record = new IkeyRecord();
        record.setJid(jid);
        record.setTimestamp(timestamp);
        record.setSuperordinate(publicKeys);
        record.getSubordinates().addAll(subordinateRecords);
        return record;
    }

    private boolean isContenderElement(IkeyRecord potentialContenderElement, IkeyRecord record) {
        if (record == null) {
            return false;
        }
        PGPPublicKeyRing potentialContenderKeys = potentialContenderElement.getSuperordinate();
        PGPPublicKeyRing previousKeys = record.getSuperordinate();

        return potentialContenderKeys.getPublicKey().getKeyID() != previousKeys.getPublicKey().getKeyID();
    }

    public IkeyRecord getIkeyElementOf(EntityBareJid from)
            throws IOException, InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
            SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException, PGPException {
        IkeyRecord stored = store.loadIkeyRecord(from);
        if (stored == null) {
            IkeyElement element = fetchIkeyElementOf(from);
            stored = elementToRecord(element);
            store.storeIkeyRecord(from, stored);
        }
        return stored;
    }

    private boolean verifyIkeyElement(EntityBareJid from, IkeyElement element)
            throws IOException, UnsupportedSignatureAlgorithmException {
        IkeySignatureVerificationMechanism verificationMechanism = getSignatureVerificationMechanismFor(element);
        IkeySignatureVerifier verifier = new IkeySignatureVerifier(verificationMechanism);
        return verifier.verify(element, from);
    }

    private static IkeySignatureVerificationMechanism getSignatureVerificationMechanismFor(IkeyElement ikeyElement)
            throws IOException, UnsupportedSignatureAlgorithmException {
        switch (ikeyElement.getType()) {
            case OX:
                PGPPublicKeyRing ikey = PGPainless.readKeyRing().publicKeyRing(ikeyElement.getSuperordinate().getPubKeyBytes());
                return new OxIkeySignatureVerificationMechanism(ikey);

            default:
                throw new UnsupportedSignatureAlgorithmException(ikeyElement.getType().name());
        }
    }

    private static boolean isFromTheFuture(IkeyRecord record) {
        Date timestamp = record.getTimestamp();
        Date now = new Date();
        return timestamp.after(now);
    }

    private boolean existsSameOrNewerRecord(IkeyRecord record, IkeyRecord existingRecord) throws IOException {
        if (existingRecord == null) {
            return false;
        }
        Date latestTimestamp = existingRecord.getTimestamp();
        Date eventTimestamp = record.getTimestamp();
        return latestTimestamp.equals(eventTimestamp) // same
                || latestTimestamp.after(eventTimestamp); // newer
    }

    @SuppressWarnings("UnnecessaryAnonymousClass")
    private final PepEventListener<IkeyElement> ikeyPepEventListener = new PepEventListener<IkeyElement>() {
        @Override
        public void onPepEvent(EntityBareJid from, IkeyElement event, String id, Message carrierMessage) {
            try {
                processIkeyElement(from, event);
            } catch (IOException | UnsupportedSignatureAlgorithmException e) {
                LOGGER.log(Level.WARNING, "Error:", e);
            } catch (PGPException e) {
                e.printStackTrace();
            }
        }
    };

    public boolean hasStore() {
        return store != null;
    }

    public boolean hasLocalKey() {
        return store.loadSecretKey() != null;
    }

    public void generateIdentityKey() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
        PGPKeyRing key = OpenPgpManager.getInstanceFor(connection())
                .generateKeyRing(connection().getUser().asBareJid());
        store.storeSecretKey(key.getSecretKeys());
        store.storeBackupPassphrase(generateBackupPassphrase());
    }

    private OpenPgpSecretKeyBackupPassphrase generateBackupPassphrase() {
        return backupPassphraseGenerator.generateBackupPassphrase();
    }

    public void restoreSecretKeyBackup(SecretkeyElement secretkeyElement, OpenPgpSecretKeyBackupPassphrase passphrase)
            throws PGPException, IOException, InvalidBackupCodeException {
        PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(secretkeyElement, passphrase);
        store.storeSecretKey(secretKeys);
        store.storeBackupPassphrase(passphrase);
    }
}
