package org.mercury_im.messenger.data.repository;

import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.data.model.AnnouncedOpenPgpContactKey;
import org.mercury_im.messenger.data.model.OpenPgpKeyFetchDate;
import org.mercury_im.messenger.data.model.OpenPgpKeyTrust;
import org.mercury_im.messenger.data.model.OpenPgpPublicKeyRing;
import org.mercury_im.messenger.data.model.OpenPgpSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpV4Fingerprint;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
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 io.requery.Persistable;
import io.requery.query.ResultDelegate;
import io.requery.query.Tuple;
import io.requery.reactivex.ReactiveEntityStore;

public class RxOpenPgpRepository implements OpenPgpRepository {

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

    private final ReactiveEntityStore<Persistable> data;

    private final AccountRepository accountRepository;

    @Inject
    public RxOpenPgpRepository(ReactiveEntityStore<Persistable> data, AccountRepository accountRepository) {
        this.data = data;
        this.accountRepository = accountRepository;
    }

    @Override
    public Completable storePublicKeysOf(UUID accountId, EntityBareJid owner, PGPPublicKeyRingCollection keys) {
        return Single.fromCallable(() -> {
            OpenPgpPublicKeyRing keyRing = new OpenPgpPublicKeyRing();
            keyRing.setAccountId(accountId);
            keyRing.setOwner(owner);
            keyRing.setBytes(keys.getEncoded());
            return keyRing;
        }).flatMap(data::upsert).ignoreElement()
                .doOnComplete(() -> LOGGER.log(Level.INFO, "Successfully stored public keys of " + owner + " for account " + accountId));
    }

    @Override
    public Single<PGPPublicKeyRingCollection> loadPublicKeysOf(UUID accountId, EntityBareJid owner) {
        return data.select(OpenPgpPublicKeyRing.class)
                .where(OpenPgpPublicKeyRing.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpPublicKeyRing.OWNER.eq(owner))
                .limit(1).get()
                .maybe().toSingle()
                .map(keyring -> PGPainless.readKeyRing().publicKeyRingCollection(keyring.getBytes()));
    }

    @Override
    public Single<Integer> deletePublicKeysOf(UUID accountId, EntityBareJid owner) {
        return data.delete(OpenPgpPublicKeyRing.class)
                .where(OpenPgpPublicKeyRing.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpPublicKeyRing.OWNER.eq(owner))
                .get().single()
                .doOnSuccess(count -> LOGGER.log(Level.INFO, "Successfully deleted " + count + " public keys of " + owner + " for account " + accountId));
    }

    @Override
    public Completable storeSecretKeysOf(UUID accountId, EntityBareJid owner, PGPSecretKeyRingCollection keys) {
        return Single.fromCallable(() -> {
            OpenPgpSecretKeyRing keyRing = new OpenPgpSecretKeyRing();
            keyRing.setAccountId(accountId);
            keyRing.setOwner(owner);
            keyRing.setBytes(keys.getEncoded());
            return keyRing;
        }).flatMap(data::upsert).ignoreElement()
                .doOnComplete(() -> LOGGER.log(Level.INFO, "Successfully stored secret keys of " + owner + " for account " + accountId));
    }

    @Override
    public Single<PGPSecretKeyRingCollection> loadSecretKeysOf(UUID accountId, EntityBareJid owner) {
        return data.select(OpenPgpSecretKeyRing.class)
                .where(OpenPgpSecretKeyRing.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpSecretKeyRing.OWNER.eq(owner))
                .limit(1).get()
                .maybe().toSingle()
                .map(keyring -> PGPainless.readKeyRing().secretKeyRingCollection(keyring.getBytes()));
    }

    @Override
    public Single<Integer> deleteSecretKeysOf(UUID accountId, EntityBareJid owner) {
        return data.delete(OpenPgpSecretKeyRing.class)
                .where(OpenPgpSecretKeyRing.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpSecretKeyRing.OWNER.eq(owner))
                .get().single()
                .doOnSuccess(count -> LOGGER.log(Level.INFO, "Successfully deleted " + count + " secret keys of " + owner + " for account " + accountId));
    }

    @Override
    public Completable storePublicKeyFetchDates(UUID accountId, EntityBareJid owner, Map<OpenPgpV4Fingerprint, Date> dates) {
        List<OpenPgpKeyFetchDate> entities = new LinkedList<>();
        for (Map.Entry<OpenPgpV4Fingerprint, Date> entry : dates.entrySet()) {
            OpenPgpKeyFetchDate entity = new OpenPgpKeyFetchDate();
            entity.setAccountId(accountId);
            entity.setOwner(owner);
            entity.setFingerprint(entry.getKey());
            entity.setFetchDate(entry.getValue());
            entities.add(entity);
        }
        return data.upsert(entities).ignoreElement();
    }

    @Override
    public Completable storeAnnouncedFingerprints(UUID accountId, EntityBareJid owner, Map<OpenPgpV4Fingerprint, Date> metadata) {
        List<AnnouncedOpenPgpContactKey> entities = new LinkedList<>();
        for (Map.Entry<OpenPgpV4Fingerprint, Date> entry : metadata.entrySet()) {
            AnnouncedOpenPgpContactKey entity = new AnnouncedOpenPgpContactKey();
            entity.setAccountId(accountId);
            entity.setOwner(owner);
            entity.setFingerprint(entry.getKey());
            entity.setModificationDate(entry.getValue());
            entities.add(entity);
        }
        return data.upsert(entities).ignoreElement()
                .doOnComplete(() -> LOGGER.log(Level.INFO, "Successfully stored announced fingerprints of " +
                        owner + " for account " + accountId + ": " + Arrays.toString(metadata.keySet().toArray())));
    }

    @Override
    public Single<Map<OpenPgpV4Fingerprint, Date>> loadAnnouncedFingerprints(UUID accountId, EntityBareJid contact) {
        return data.select(AnnouncedOpenPgpContactKey.class)
                .where(AnnouncedOpenPgpContactKey.ACCOUNT_ID.eq(accountId))
                .and(AnnouncedOpenPgpContactKey.OWNER.eq(contact))
                .get()
                .observableResult()
                .map(ResultDelegate::toList)
                .single(Collections.emptyList())
                .map(list -> {
                    Map<OpenPgpV4Fingerprint, Date> map = new ConcurrentHashMap<>();
                    for (AnnouncedOpenPgpContactKey key : list) {
                        map.put(key.getFingerprint(), key.getModificationDate());
                    }
                    return map;
                });
    }

    @Override
    public Single<Date> loadAnnouncementDate(UUID accountId, EntityBareJid owner, OpenPgpV4Fingerprint fingerprint) {
        return data.select(AnnouncedOpenPgpContactKey.class)
                .where(AnnouncedOpenPgpContactKey.ACCOUNT_ID.eq(accountId)
                        .and(AnnouncedOpenPgpContactKey.OWNER.eq(owner))
                        .and(AnnouncedOpenPgpContactKey.FINGERPRINT.eq(fingerprint)))
                .limit(1)
                .get()
                .maybe()
                .toSingle()
                .map(AnnouncedOpenPgpContactKey::getModificationDate);
    }

    @Override
    public Single<Date> loadAnnouncementDate(UUID accountId, OpenPgpV4Fingerprint fingerprint) {
        return accountRepository.getAccount(accountId).toSingle()
                .flatMap(account -> loadAnnouncementDate(accountId, account.getJid(), fingerprint));
    }

    @Override
    public Single<OpenPgpTrustStore.Trust> loadTrust(UUID accountId, EntityBareJid owner, OpenPgpV4Fingerprint fingerprint) {
        return data.select(OpenPgpKeyTrust.class)
                .where(OpenPgpKeyTrust.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpKeyTrust.OWNER.eq(owner))
                .and(OpenPgpKeyTrust.FINGERPRINT.eq(fingerprint))
                .limit(1)
                .get()
                .maybe()
                .toSingle(new OpenPgpKeyTrust())
                .map(entity -> entity.getTrust() != null ? entity.getTrust() : OpenPgpTrustStore.Trust.undecided);
    }

    @Override
    public Single<OpenPgpTrustStore.Trust> loadTrust(UUID accountId, OpenPgpV4Fingerprint fingerprint) {
        return accountRepository.getAccount(accountId).toSingle()
                .flatMap(account -> loadTrust(accountId, account.getJid(), fingerprint));
    }

    @Override
    public Completable storeTrust(UUID accountId, EntityBareJid owner, OpenPgpV4Fingerprint fingerprint, OpenPgpTrustStore.Trust trust) {
        OpenPgpKeyTrust entity = new OpenPgpKeyTrust();
        entity.setAccountId(accountId);
        entity.setOwner(owner);
        entity.setFingerprint(fingerprint);
        entity.setTrust(trust);
        return data.upsert(entity).ignoreElement();
    }

    @Override
    public Completable storeTrust(UUID accountId, OpenPgpV4Fingerprint fingerprint, OpenPgpTrustStore.Trust trust) {
        return accountRepository.getAccount(accountId).toSingle()
                .flatMapCompletable(account -> storeTrust(accountId, account.getJid(), fingerprint, trust));
    }

    @Override
    public Observable<List<OpenPgpV4Fingerprint>> observeFingerprintsOf(UUID accountId, String peerAddress) {
        return data.select(OpenPgpPublicKeyRing.class)
                .where(OpenPgpPublicKeyRing.ACCOUNT_ID.eq(accountId))
                .and(OpenPgpPublicKeyRing.OWNER.eq(JidCreate.entityBareFromOrThrowUnchecked(peerAddress)))
                .get().observableResult()
                .map(result -> {
                    OpenPgpPublicKeyRing model = new ResultDelegate<>(result).firstOrNull();
                    if (model == null) {
                        return Collections.emptyList();
                    }
                    List<OpenPgpV4Fingerprint> fingerprints = new ArrayList<>();
                    PGPPublicKeyRingCollection keys = PGPainless.readKeyRing().publicKeyRingCollection(model.getBytes());
                    for (PGPPublicKeyRing key : keys) {
                        fingerprints.add(new OpenPgpV4Fingerprint(key));
                    }
                    return fingerprints;
                });
    }

    @Override
    public Observable<Optional<OpenPgpV4Fingerprint>> observeLocalFingerprintOf(UUID accountId) {
        return data.select(OpenPgpSecretKeyRing.class)
                .where(OpenPgpSecretKeyRing.ACCOUNT_ID.eq(accountId))
                .get().observableResult()
                .map(result -> {
                    OpenPgpSecretKeyRing ring = new ResultDelegate<>(result).firstOrNull();
                    if (ring == null) {
                        return new Optional<>();
                    } else {
                        return new Optional<>(new OpenPgpV4Fingerprint(
                                PGPainless.readKeyRing().secretKeyRing(ring.getBytes()))
                        );
                    }
                });
    }

    @Override
    public Observable<List<FingerprintViewItem>> observeFingerprints(UUID accountId, EntityBareJid owner) {
        return data.select(AnnouncedOpenPgpContactKey.ACCOUNT_ID,
                AnnouncedOpenPgpContactKey.OWNER,
                AnnouncedOpenPgpContactKey.FINGERPRINT,
                AnnouncedOpenPgpContactKey.MODIFICATION_DATE,
                OpenPgpKeyFetchDate.FETCH_DATE,
                OpenPgpKeyTrust.TRUST)

                .from(AnnouncedOpenPgpContactKey.class)
                .leftJoin(OpenPgpKeyFetchDate.class)
                .on(AnnouncedOpenPgpContactKey.ACCOUNT_ID.eq(OpenPgpKeyFetchDate.ACCOUNT_ID)
                        .and(AnnouncedOpenPgpContactKey.OWNER.eq(OpenPgpKeyFetchDate.OWNER)
                                .and(AnnouncedOpenPgpContactKey.FINGERPRINT.eq(OpenPgpKeyFetchDate.FINGERPRINT))))

                .leftJoin(OpenPgpKeyTrust.class)
                .on(OpenPgpKeyTrust.ACCOUNT_ID.eq(AnnouncedOpenPgpContactKey.ACCOUNT_ID)
                        .and(OpenPgpKeyTrust.OWNER.eq(AnnouncedOpenPgpContactKey.OWNER)
                                .and(OpenPgpKeyTrust.FINGERPRINT.eq(AnnouncedOpenPgpContactKey.FINGERPRINT))))

                .where(AnnouncedOpenPgpContactKey.ACCOUNT_ID.eq(accountId).and(AnnouncedOpenPgpContactKey.OWNER.eq(owner)))
                .orderBy(AnnouncedOpenPgpContactKey.MODIFICATION_DATE.desc())

                .get().observableResult()
                .map(ResultDelegate::toList)
                .map(list -> {
                    ArrayList<FingerprintViewItem> result = new ArrayList<>(list.size());
                    for (Tuple tuple : list) {
                        result.add(new FingerprintViewItem(tuple.get(0), tuple.get(1), tuple.get(2), tuple.get(3), tuple.get(4), tuple.get(5)));
                    }
                    return result;
                });
    }

    @Override
    public Single<Map<OpenPgpV4Fingerprint, Date>> loadPublicKeyFetchDates(UUID accountId, EntityBareJid owner) {
        List<OpenPgpKeyFetchDate> fetchDates =
                data.select(OpenPgpKeyFetchDate.class)
                        .where(OpenPgpKeyFetchDate.ACCOUNT_ID.eq(accountId))
                        .and(OpenPgpKeyFetchDate.OWNER.eq(owner))
                        .get()
                        .toList();

        Map<OpenPgpV4Fingerprint, Date> map = new ConcurrentHashMap<>();
        for (OpenPgpKeyFetchDate date : fetchDates) {
            map.put(date.getFingerprint(), date.getFetchDate());
        }
        return Single.just(map);
    }
}
