package org.mercury_im.messenger.core.store.crypto;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpKeyStore;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpMetadataStore;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpStore;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpTrustStore;
import org.jxmpp.jid.BareJid;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.pgpainless.key.OpenPgpV4Fingerprint;

import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.reactivex.disposables.CompositeDisposable;

public class MercuryOpenPgpStore extends AbstractOpenPgpStore {

    protected static final Logger LOGGER = Logger.getLogger(MercuryOpenPgpStore.class.getName());

    public MercuryOpenPgpStore(UUID accountId, OpenPgpRepository repository, SchedulersFacade schedulers) {
        super(
                new KeyStore(accountId, repository, schedulers),
                new MetadataStore(accountId, repository, schedulers),
                new TrustStore(accountId, repository, schedulers));
    }

    private static class KeyStore extends AbstractOpenPgpKeyStore {

        private final CompositeDisposable disposable = new CompositeDisposable();
        private final OpenPgpRepository repository;
        private final SchedulersFacade schedulers;

        private final UUID accountId;


        public KeyStore(UUID accountId, OpenPgpRepository repository, SchedulersFacade schedulers) {
            this.accountId = accountId;
            this.repository = repository;
            this.schedulers = schedulers;
        }

        @Override
        protected PGPPublicKeyRingCollection readPublicKeysOf(BareJid owner)
                throws IOException, PGPException {
            try {
                return repository.loadPublicKeysOf(accountId, owner.asEntityBareJidIfPossible())
                        .blockingGet();
            } catch (NoSuchElementException e) {
                return null;
            }
        }

        @Override
        protected void writePublicKeysOf(BareJid owner, PGPPublicKeyRingCollection publicKeys)
                throws IOException {
            disposable.add(repository.storePublicKeysOf(accountId, owner.asEntityBareJidIfPossible(), publicKeys)
                    .subscribeOn(schedulers.getIoScheduler())
                    .subscribe(
                            () -> MercuryOpenPgpStore.LOGGER.log(Level.FINER, "Successfully wrote OX public keys for " + owner + " (accountId=" + accountId + ")"),
                            e -> MercuryOpenPgpStore.LOGGER.log(Level.SEVERE, "Error writing OX public keys for " + owner + " (accountId=" + accountId + ")")
                    ));
        }

        @Override
        protected PGPSecretKeyRingCollection readSecretKeysOf(BareJid owner)
                throws IOException, PGPException {
            try {
                return repository.loadSecretKeysOf(accountId, owner.asEntityBareJidIfPossible())
                        .blockingGet();
            } catch (NoSuchElementException e) {
                return null;
            }
        }

        @Override
        protected void writeSecretKeysOf(BareJid owner, PGPSecretKeyRingCollection secretKeys)
                throws IOException {
            disposable.add(repository.storeSecretKeysOf(accountId, owner.asEntityBareJidIfPossible(), secretKeys)
                    .subscribeOn(schedulers.getIoScheduler())
                    .subscribe(
                            () -> MercuryOpenPgpStore.LOGGER.log(Level.FINER, "Successfully wrote OX secret keys for " + owner + " (accountId=" + accountId + ")"),
                            e -> MercuryOpenPgpStore.LOGGER.log(Level.SEVERE, "Error writing OX secret keys for " + owner + " (accountId=" + accountId + ")")
                    ));
        }

        @Override
        protected Map<OpenPgpV4Fingerprint, Date> readKeyFetchDates(BareJid owner)
                throws IOException {
            return repository.loadPublicKeyFetchDates(accountId, owner.asEntityBareJidIfPossible())
                    .blockingGet();
        }

        @Override
        protected void writeKeyFetchDates(BareJid owner, Map<OpenPgpV4Fingerprint, Date> dates)
                throws IOException {
            disposable.add(repository.storePublicKeyFetchDates(accountId, owner.asEntityBareJidIfPossible(), dates)
                    .subscribeOn(schedulers.getIoScheduler())
                    .subscribe(
                            () -> MercuryOpenPgpStore.LOGGER.log(Level.FINER, "Successfully updated OX fetch dates for " + owner + " (accountId=" + accountId + ")"),
                            e -> MercuryOpenPgpStore.LOGGER.log(Level.SEVERE, "Error updating OX key fetch dates for " + owner + " (accountId=" + accountId + ")")
                    ));
        }

        @Override
        protected void finalize() throws Throwable {
            disposable.dispose();
            super.finalize();
        }
    }

    private static class MetadataStore extends AbstractOpenPgpMetadataStore {

        private final CompositeDisposable disposable = new CompositeDisposable();
        private final OpenPgpRepository repository;
        private final SchedulersFacade schedulers;

        private final UUID accountId;

        public MetadataStore(UUID accountId, OpenPgpRepository repository, SchedulersFacade schedulers) {
            this.accountId = accountId;
            this.repository = repository;
            this.schedulers = schedulers;
        }

        @Override
        protected Map<OpenPgpV4Fingerprint, Date> readAnnouncedFingerprintsOf(BareJid contact)
                throws IOException {
            return repository.loadAnnouncedFingerprints(accountId, contact.asEntityBareJidIfPossible())
                    .blockingGet();
        }

        @Override
        protected void writeAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> metadata)
                throws IOException {
            disposable.add(repository.storeAnnouncedFingerprints(accountId, contact.asEntityBareJidIfPossible(), metadata)
                    .subscribeOn(schedulers.getIoScheduler())
                    .subscribe(
                            () -> MercuryOpenPgpStore.LOGGER.log(Level.FINER, "Successfully updated announced OX fingerprints for " + contact + " (accountId=" + accountId + ")"),
                            e -> MercuryOpenPgpStore.LOGGER.log(Level.SEVERE, "Error updating announced OX fingerprints for " + contact + " (accountId=" + accountId + ")")
                    ));
        }

        @Override
        protected void finalize() throws Throwable {
            disposable.dispose();
            super.finalize();
        }
    }

    public static class TrustStore extends AbstractOpenPgpTrustStore {

        private final CompositeDisposable disposable = new CompositeDisposable();
        private final OpenPgpRepository repository;
        private final SchedulersFacade schedulers;

        private final UUID accountId;

        public TrustStore(UUID accountId, OpenPgpRepository repository, SchedulersFacade schedulers) {
            this.accountId = accountId;
            this.repository = repository;
            this.schedulers = schedulers;
        }

        @Override
        protected Trust readTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException {
            return repository.loadTrust(accountId, owner.asEntityBareJidIfPossible(), fingerprint)
                    .blockingGet();
        }

        @Override
        protected void writeTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException {
            disposable.add(repository.storeTrust(accountId, owner.asEntityBareJidIfPossible(), fingerprint, trust)
            .subscribeOn(schedulers.getIoScheduler())
            .subscribe(
                    () -> MercuryOpenPgpStore.LOGGER.log(Level.FINER, "Successfully set trust in key " + fingerprint + " to " + trust + " for " + owner + " (accountId=" + accountId + ")"),
                    e -> MercuryOpenPgpStore.LOGGER.log(Level.SEVERE, "Error setting trust in key " + fingerprint + " to " + trust + " for " + owner + " (accountId=" + accountId + ")")
            ));
        }

        @Override
        protected void finalize() throws Throwable {
            disposable.dispose();
            super.finalize();
        }
    }
}
