package org.mercury_im.messenger.data.repository;

import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smackx.ikey.record.IkeyRecord;
import org.jivesoftware.smackx.ikey.record.IkeySubordinateRecord;
import org.jivesoftware.smackx.ikey.record.OxSubordinateRecord;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.data.model.IkeyRecordModel;
import org.mercury_im.messenger.data.model.IkeySecretKeyModel;
import org.mercury_im.messenger.data.model.IkeySubordinateModel;
import org.pgpainless.key.OpenPgpV4Fingerprint;

import java.util.ArrayList;
import java.util.Arrays;
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 io.requery.Persistable;
import io.requery.query.ResultDelegate;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveResult;

public class RxIkeyRepository implements IkeyRepository {

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

    private final ReactiveEntityStore<Persistable> data;

    @Inject
    public RxIkeyRepository(ReactiveEntityStore<Persistable> data) {
        this.data = data;
    }

    @Override
    public Observable<Optional<PGPSecretKeyRing>> loadSecretKey(UUID accountId) {
        return data.select(IkeySecretKeyModel.class)
                .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId))
                .get()
                .observableResult()
                .map(ResultDelegate::toList)
                .doOnNext(l -> LOGGER.log(Level.INFO, "new Result: " + Arrays.toString(l.toArray())))
                .map(l -> l.isEmpty() ? new Optional<>() : new Optional<>(l.get(0).getKey()));
    }

    @Override
    public Completable storeSecretKey(UUID accountId, PGPSecretKeyRing secretKey) {

        return data.select(IkeySecretKeyModel.class)
                .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId))
                .get().observable()
                .single(new IkeySecretKeyModel())
                .map(m -> {
                    m.setAccountId(accountId);
                    m.setKey(secretKey);
                    m.setFingerprint(new OpenPgpV4Fingerprint(secretKey.getPublicKey()));
                    return m;
                })
                .flatMap(data::upsert)
                .ignoreElement();
    }

    @Override
    public Single<Integer> deleteSecretKey(UUID accountId) {
        return data.delete().from(IkeySecretKeyModel.class)
                .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId))
                .get()
                .single();
    }

    @Override
    public Single<Optional<OpenPgpSecretKeyBackupPassphrase>> loadBackupPassphrase(UUID accountID) {
        return data.select(IkeySecretKeyModel.class)
                .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountID))
                .get()
                .observable()
                .map(m -> new Optional<>(m.getBackupPassphrase()))
                .single(new Optional<>());
    }

    @Override
    public Completable storeBackupPassphrase(UUID accountId, OpenPgpSecretKeyBackupPassphrase passphrase) {
        return data.select(IkeySecretKeyModel.class)
                .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId))
                .get().observable()
                .single(new IkeySecretKeyModel())
                .map(m -> {
                    m.setAccountId(accountId);
                    m.setBackupPassphrase(passphrase);
                    return m;
                })
                .flatMap(data::upsert)
                .ignoreElement();
    }

    @Override
    public Observable<Optional<OpenPgpTrustStore.Trust>> loadSuperordinateTrust(UUID accountId, EntityBareJid jid, OpenPgpV4Fingerprint fingerprint) {
        return loadRecord(accountId, jid)
                .map(model -> {
                    if (fingerprint.equals(new OpenPgpV4Fingerprint(model.getSuperordinate()))) {
                        return new Optional<>(model.getTrust());
                    } else {
                        return new Optional<>();
                    }
                });
    }

    @Override
    public Completable storeSuperordinateTrust(UUID accountId, EntityBareJid jid, OpenPgpV4Fingerprint fingerprint, OpenPgpTrustStore.Trust trust) {
        return loadRecord(accountId, jid)
                .singleOrError()
                .map(record -> {
                    record.setTrust(trust);
                    return record;
                })
                .flatMapCompletable(record -> storeRecord(accountId, jid, record));
    }

    @Override
    public Observable<IkeyRecord> loadRecord(UUID accountId, EntityBareJid jid) {
        return getRecordModel(accountId, jid, false)
                .map(m -> {
                    List<IkeySubordinateRecord> subordinateRecords = new ArrayList<>();
                    for (IkeySubordinateModel sub : m.getSubordinates()) {
                        if (sub.getType().equals(OpenPgpElement.NAMESPACE)) {
                            OxSubordinateRecord r = new OxSubordinateRecord();
                            r.setId(sub.getId());
                            r.setOxFingerprint(new OpenPgpV4Fingerprint(sub.getFpr()));
                            r.setUri(sub.getUri());
                            subordinateRecords.add(r);
                        }
                    }
                    return new IkeyRecord(jid, m.getTimestamp(), m.getSuperordinate(), subordinateRecords, m.getTrust());
                })
                .doOnError(e -> LOGGER.log(Level.SEVERE, "Error loading ikey record", e));
    }

    @Override
    public Completable storeRecord(UUID accountId, EntityBareJid jid, IkeyRecord record) {
        assert jid.equals(record.getJid());
        return getRecordModel(accountId, jid, false)
                .single(new IkeyRecordModel())
                .map(m -> {
                    if (m.getId() == null) m.setId(UUID.randomUUID());
                    m.setAccountId(accountId);
                    m.setJid(jid);
                    m.setContender(false);
                    m.setFingerprint(new OpenPgpV4Fingerprint(record.getSuperordinate()));
                    m.setSuperordinate(record.getSuperordinate());
                    m.setTimestamp(record.getTimestamp());
                    m.setTrust(record.getTrust());

                    m.getSubordinates().clear();
                    for (IkeySubordinateRecord s : record.getSubordinates()) {
                        IkeySubordinateModel sm = new IkeySubordinateModel();
                        sm.setId(s.getId());
                        sm.setRecord(m);
                        sm.setFpr(s.getFingerprint());
                        sm.setType(s.getType());
                        sm.setUri(s.getUri());
                        m.getSubordinates().add(sm);
                    }

                    return m;
                })
                .flatMap(data::upsert)
                .ignoreElement()
                .doOnError(e -> LOGGER.log(Level.SEVERE, "Error storing ikey record", e));
    }

    @Override
    public Observable<IkeyRecord> loadContenderRecord(UUID accountId, EntityBareJid jid) {
        return getRecordModel(accountId, jid, true)
                .map(m -> {
                    List<IkeySubordinateRecord> subordinateRecords = new ArrayList<>();
                    for (IkeySubordinateModel s : m.getSubordinates()) {
                        if (s.getType().equals(OpenPgpElement.NAMESPACE)) {
                            OxSubordinateRecord sr = new OxSubordinateRecord();
                            sr.setUri(s.getUri());
                            sr.setOxFingerprint(new OpenPgpV4Fingerprint(s.getFpr()));
                            subordinateRecords.add(sr);
                        }
                    }
                    return new IkeyRecord(m.getJid(), m.getTimestamp(), m.getSuperordinate(), subordinateRecords, m.getTrust());
                })
                .doOnError(e -> LOGGER.log(Level.SEVERE, "Error loading contender ikey record", e));
    }

    @Override
    public Completable storeContenderRecord(UUID accountId, EntityBareJid jid, IkeyRecord record) {
        assert jid.equals(record.getJid());
        return getRecordModel(accountId, jid, true)
                .single(new IkeyRecordModel())
                .map(m -> {
                    if (m.getId() == null) m.setId(UUID.randomUUID());
                    m.setAccountId(accountId);
                    m.setJid(jid);
                    m.setContender(true);
                    m.setFingerprint(new OpenPgpV4Fingerprint(record.getSuperordinate()));
                    m.setSuperordinate(record.getSuperordinate());
                    m.setTimestamp(record.getTimestamp());
                    m.setTrust(record.getTrust());

                    for (IkeySubordinateRecord s : record.getSubordinates()) {
                        IkeySubordinateModel sm = new IkeySubordinateModel();
                        sm.setId(UUID.randomUUID());
                        sm.setRecord(m);
                        sm.setFpr(s.getFingerprint());
                        sm.setType(s.getType());
                        sm.setUri(s.getUri());
                        m.getSubordinates().add(sm);
                    }

                    return m;
                })
                .flatMap(data::upsert)
                .ignoreElement()
                .doOnError(e -> LOGGER.log(Level.SEVERE, "Error storing contender ikey record", e));
    }

    @Override
    public Completable clearContenderRecord(UUID accountId, EntityBareJid jid) {
        return data.delete(IkeyRecordModel.class)
                .where(IkeyRecordModel.ACCOUNT_ID.eq(accountId)
                        .and(IkeyRecordModel.JID.eq(jid))
                .and(IkeyRecordModel.CONTENDER.eq(true)))
                .get().single().ignoreElement();
    }

    // TODO: When this method is used in loadRecord() to get continuous updates of the record model
    //  unfortunately updating the model (eg. per storeSuperordinateTrust() ) no longer terminates.
    private Observable<IkeyRecordModel> getContinuousRecordModel(UUID accountId, EntityBareJid jid, boolean isContender) {
        return getRecordModelResult(accountId, jid, isContender)
                .observableResult()
                .map(ResultDelegate::first);
    }

    private Observable<IkeyRecordModel> getRecordModel(UUID accountId, EntityBareJid jid, boolean isContender) {
        return getRecordModelResult(accountId, jid, isContender)
                .observable();
    }

    private ReactiveResult<IkeyRecordModel> getRecordModelResult(UUID accountId, EntityBareJid jid, boolean isContender) {
        return data.select(IkeyRecordModel.class)
                .where(IkeyRecordModel.ACCOUNT_ID.eq(accountId)
                        .and(IkeyRecordModel.JID.eq(jid))
                        .and(IkeyRecordModel.CONTENDER.eq(isContender)))
                .get();
    }

}
