package org.mercury_im.messenger.core.viewmodel.account.detail;

import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.PresenceBuilder;
import org.jivesoftware.smackx.ikey.IkeyManager;
import org.jivesoftware.smackx.ikey.element.IkeyElement;
import org.jivesoftware.smackx.ikey.element.SubordinateElement;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.pep.PepManager;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.pubsub.PubSubUri;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.connection.state.ConnectionState;
import org.mercury_im.messenger.core.connection.state.ConnectivityState;
import org.mercury_im.messenger.core.crypto.ikey.IkeyInitializer;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpTrustRepository;
import org.mercury_im.messenger.core.store.crypto.IkeyAwareOpenPgpStore;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.Account;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.protection.UnprotectedKeysProtector;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;

import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;

import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS;

public class AccountDetailsViewModel implements MercuryViewModel {

    private static final Logger LOGGER = Logger.getLogger(AccountDetailsViewModel.class.getName());

    private MercuryConnectionManager connectionManager;
    private final OpenPgpRepository openPgpRepository;
    private final OpenPgpTrustRepository deviceKeyTrustRepository;
    private final IkeyRepository ikeyRepository;
    private final AccountRepository accountRepository;
    private final SchedulersFacade schedulers;
    private final IkeyInitializer ikeyInitializer;

    @Inject
    public AccountDetailsViewModel(MercuryConnectionManager connectionManager,
                                   OpenPgpRepository openPgpRepository,
                                   OpenPgpTrustRepository deviceKeyTrustRepository,
                                   IkeyRepository ikeyRepository,
                                   AccountRepository accountRepository,
                                   SchedulersFacade schedulers,
                                   IkeyInitializer ikeyInitializer) {
        this.connectionManager = connectionManager;
        this.openPgpRepository = openPgpRepository;
        this.deviceKeyTrustRepository = deviceKeyTrustRepository;
        this.ikeyRepository = ikeyRepository;
        this.accountRepository = accountRepository;
        this.schedulers = schedulers;
        this.ikeyInitializer =  ikeyInitializer;
    }

    public void markFingerprintTrusted(UUID accountId, OpenPgpV4Fingerprint fingerprint, boolean trusted) {
        OpenPgpTrustStore.Trust trust = trusted ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted;
        addDisposable(accountRepository.getAccount(accountId)
                .toSingle()
                .flatMapCompletable(account -> deviceKeyTrustRepository
                        .storeTrust(account.getId(), account.getJid(), fingerprint, trust))
                .compose(schedulers.executeUiSafeCompletable())
                .subscribe(() -> LOGGER.log(Level.INFO, "Fingerprint " + fingerprint + " marked " + trust),
                        e -> LOGGER.log(Level.SEVERE, "An exception occurred marking fingerprint " + fingerprint + " as " + trusted, e)));
    }

    public void sendIkeyElement(UUID accountId) {
        addDisposable(Completable
                .fromAction(() -> {
                    IkeyElement ikeyElement = syncCreateIkeyElement(accountId);
                    IkeyManager ikeyManager = IkeyManager.getInstanceFor(connectionManager.getConnection(accountId).getConnection());

                    ikeyManager.storeAndPublishElement(ikeyElement);
                })
                .compose(schedulers.executeUiSafeCompletable())
                .subscribe(
                        () -> LOGGER.log(Level.INFO, "Successfully published ikey element."),
                        e -> LOGGER.log(Level.SEVERE, "Error publishing ikey element:", e)));
    }

    private IkeyElement syncCreateIkeyElement(UUID accountId) throws URISyntaxException, IOException {
        MercuryConnection connection = connectionManager.getConnection(accountId);
        IkeyManager ikeyManager = ikeyInitializer.initFor(connection);

        Account account = accountRepository.getAccount(accountId).blockingGet();
        OpenPgpV4Fingerprint localFp = openPgpRepository.observeLocalFingerprintOf(accountId).blockingFirst().getItem();
        // Why does openPgpRepository.loadAnnouncedFingerprints() never return?
        List<OpenPgpV4Fingerprint> fingerprintList =
                openPgpRepository.observeFingerprints(accountId, account.getJid()).first(new ArrayList<>())
                        .map(l -> {
                            List<OpenPgpV4Fingerprint> fps = new ArrayList<>();
                            for (FingerprintViewItem vi : l) {
                                if (vi.getTrusted() == OpenPgpTrustStore.Trust.trusted) {
                                    fps.add(vi.getFingerprint());
                                }
                            }
                            return fps;
                        }).blockingGet();

        List<SubordinateElement> subordinateElements = new ArrayList<>();
        URI uri = new URI(getOxKeyNodeUri(connection, localFp).toString());
        subordinateElements.add(new SubordinateElement("urn:xmpp:openpgp:0", uri, localFp.toString()));

        for (OpenPgpV4Fingerprint fp : fingerprintList) {
            URI u = new URI(getOxKeyNodeUri(connection, fp).toString());
            subordinateElements.add(new SubordinateElement("urn:xmpp:openpgp:0", u, fp.toString()));
        }

        PGPSecretKeyRing secretKeys = ikeyRepository.loadSecretKey(accountId)
                .blockingFirst(new Optional<>())
                .getItem();

        IkeyElement ikeyElement = ikeyManager.createOxIkeyElement(secretKeys,
                new UnprotectedKeysProtector(), subordinateElements.toArray(new SubordinateElement[]{}));

        return ikeyElement;
    }

    private PubSubUri getOxKeyNodeUri(MercuryConnection connection, OpenPgpV4Fingerprint fingerprint) {
        PubSubManager pubSubManager = PubSubManager.getInstanceFor(connection.getConnection());
        BareJid serviceJid = pubSubManager.getServiceJid();
        return new PubSubUri(serviceJid, OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEY(fingerprint), null, null);
    }

    public Single<EntityBareJid> getJid(UUID accountId) {
        return accountRepository.getAccount(accountId).toSingle()
                .compose(schedulers.executeUiSafeSingle())
                .map(Account::getJid);
    }

    public Observable<Optional<OpenPgpV4Fingerprint>> observeIkeyFingerprint(UUID accountId) {
        return ikeyRepository.loadSecretKey(accountId)
                .map(key -> key.isPresent() ? new Optional<>(new OpenPgpV4Fingerprint(key.getItem().getPublicKey())) : new Optional<>());
    }

    public Observable<Optional<OpenPgpV4Fingerprint>> observeLocalDeviceFingerprint(UUID accountId) {
        return openPgpRepository.observeLocalFingerprintOf(accountId);
    }

    public Observable<List<FingerprintViewItem>> observeRemoteDeviceFingerprints(UUID accountId) {
        OpenPgpStore store = OpenPgpManager.getInstanceFor(connectionManager.getConnection(accountId).getConnection())
                .getOpenPgpProvider().getStore();
        return observeLocalDeviceFingerprint(accountId)
                .flatMap(optional -> accountRepository.getAccount(accountId).toSingle()
                        .flatMapObservable(account -> openPgpRepository.observeFingerprints(accountId, account.getJid())
                                .map(list -> {
                                    if (!optional.isPresent()) {
                                        return list;
                                    }

                                    List<FingerprintViewItem> remoteFingerprints = new ArrayList<>();
                                    for(FingerprintViewItem f : list) {
                                        f.setTrusted(store.getTrust(f.getOwner(), f.getFingerprint()));
                                        if (!f.getFingerprint().equals(optional.getItem())) {
                                            remoteFingerprints.add(f);
                                        }
                                    }
                                    return remoteFingerprints;
                                })));
    }

    public Completable unpublishPublicKey(UUID accountId, OpenPgpV4Fingerprint fingerprint) {
        return deletePublicKeyNode(accountId, fingerprint)
                .andThen(removePublicKeyFromPubKeyList(accountId, fingerprint));
    }

    private Completable removePublicKeyFromPubKeyList(UUID accountId, OpenPgpV4Fingerprint fingerprint) {
        return Completable.fromAction(() -> {
            XMPPConnection xmppConnection = connectionManager.getConnection(accountId).getConnection();
            PepManager pepManager = PepManager.getInstanceFor(xmppConnection);
            PubSubManager pubSubManager = pepManager.getPepPubSubManager();
            PublicKeysListElement publishedKeys = OpenPgpPubSubUtil.fetchPubkeysList(xmppConnection);
            PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
            for (PublicKeysListElement.PubkeyMetadataElement meta : publishedKeys.getMetadata().values()) {
                if (meta.getV4Fingerprint().equals(fingerprint)) {
                    continue;
                }
                builder.addMetadata(meta);
            }
            publishedKeys = builder.build();

            LeafNode metadataNode = pubSubManager.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS);
            metadataNode.publish(new PayloadItem<>(publishedKeys));
        });
    }

    private Completable deletePublicKeyNode(UUID accountId, OpenPgpV4Fingerprint fingerprint) {
        return Completable.fromAction(() -> {
            XMPPConnection xmppConnection = connectionManager.getConnection(accountId).getConnection();
            PepManager pepManager = PepManager.getInstanceFor(xmppConnection);
            OpenPgpPubSubUtil.deletePublicKeyNode(pepManager, fingerprint);
        });
    }

    public Completable generateIkey(UUID accountId) {
        return Completable.fromAction(() -> {
            MercuryConnection connection = connectionManager.getConnection(accountId);
            IkeyManager ikeyManager = ikeyInitializer.initFor(connection);
            ikeyManager.generateIdentityKey();
        });
    }

    public Completable deleteIkey(UUID accountId) {
        return ikeyRepository.deleteSecretKey(accountId).ignoreElement();
    }

    public Completable restoreIkeyBackup(UUID accountId) {
        return Completable.fromAction(() -> {
            MercuryConnection connection = connectionManager.getConnection(accountId);
            IkeyManager ikeyManager = ikeyInitializer.initFor(connection);
            SecretkeyElement secretkeyElement = ikeyManager.fetchSecretIdentityKey();
        });
    }

    public Observable<Boolean> isAccountEnabled(UUID accountId) {
        return accountRepository.observeAccount(accountId)
                .filter(Optional::isPresent)
                .map(Optional::getItem)
                .map(Account::isEnabled);
    }

    public Observable<Boolean> isAccountAuthenticated(UUID accountId) {
        return connectionManager.getConnection(accountId)
                .observeConnection()
                .map(ConnectionState::getConnectivity)
                .map(connectivity -> connectivity == ConnectivityState.authenticated);
    }

    public Completable sendPresence(UUID accountId) {
        return Completable.fromAction(() -> {
            XMPPConnection connection = connectionManager.getConnection(accountId).getConnection();
            connection.sendStanza(PresenceBuilder.buildPresence().ofType(Presence.Type.available).build());
        });

    }
}
