package org.mercury_im.messenger.core.viewmodel.contact;

import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterGroup;
import org.jivesoftware.smackx.ikey.record.IkeyRecord;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
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.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.DirectChatRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpTrustRepository;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
import org.mercury_im.messenger.core.util.CombinedPresenceListener;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.util.Tuple;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.contact.Peer;
import org.pgpainless.key.OpenPgpV4Fingerprint;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
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.reactivex.functions.BiFunction;
import io.reactivex.subjects.BehaviorSubject;
import lombok.Getter;

public class ContactDetailViewModel implements MercuryViewModel {

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

    private final MercuryConnectionManager connectionManager;
    private final PeerRepository peerRepository;
    private final DirectChatRepository directChatRepository;
    private final OpenPgpRepository openPgpRepository;
    private final OpenPgpTrustRepository trustRepository;
    private final IkeyRepository ikeyRepository;
    private final SchedulersFacade schedulers;

    private Roster roster;
    private FilteredPresenceEventListener presenceEventListener;

    private BehaviorSubject<EntityBareJid> contactAddress = BehaviorSubject.create();
    private BehaviorSubject<String> contactDisplayName = BehaviorSubject.create();
    private BehaviorSubject<EntityBareJid> contactAccountAddress = BehaviorSubject.create();
    private BehaviorSubject<Presence.Mode> contactPresenceMode = BehaviorSubject.create();
    private BehaviorSubject<String> contactPresenceStatus = BehaviorSubject.create();
    private BehaviorSubject<List<String>> contactGroups = BehaviorSubject.create();
    private BehaviorSubject<Optional<FingerprintViewItem>> contactIdentityFingerprint = BehaviorSubject.createDefault(new Optional<>());
    private BehaviorSubject<List<FingerprintViewItem>> contactDeviceFingerprints = BehaviorSubject.createDefault(Collections.emptyList());
    private BehaviorSubject<Tuple<String, EntityBareJid>> contactAvatarBase = BehaviorSubject.create();

    private OpenPgpStore openPgpStore;

    @Getter
    private UUID peerId;

    @Getter
    private UUID accountId;

    @Inject
    public ContactDetailViewModel(MercuryConnectionManager connectionManager,
                                  PeerRepository peerRepository,
                                  DirectChatRepository directChatRepository,
                                  OpenPgpRepository openPgpRepository,
                                  OpenPgpTrustRepository trustRepository,
                                  IkeyRepository ikeyRepository,
                                  SchedulersFacade schedulers) {
        this.connectionManager = connectionManager;
        this.peerRepository = peerRepository;
        this.directChatRepository = directChatRepository;
        this.openPgpRepository = openPgpRepository;
        this.trustRepository = trustRepository;
        this.ikeyRepository = ikeyRepository;
        this.schedulers = schedulers;
    }

    public Completable init(UUID peerId) {
        return peerRepository.getPeer(peerId)
                .flatMapCompletable(this::init);
    }

    public Completable init(Peer peer) {
        OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(connectionManager.getConnection(peer.getAccount().getId()).getConnection());
        OpenPgpProvider provider = openPgpManager.getOpenPgpProvider();
        this.openPgpStore = provider.getStore();

        return Completable.fromAction(() -> {
            this.peerId = peer.getId();
            this.accountId = peer.getAccount().getId();

            MercuryConnection connection = connectionManager.getConnection(peer.getAccount());
            roster = Roster.getInstanceFor(connection.getConnection());
            setupPresenceEventListener(roster, peer.getJid());

            // peer
            addDisposable(peerRepository
                    .observePeer(peerId)
                    .filter(Optional::isPresent)
                    .map(Optional::getItem)
                    .compose(schedulers.executeUiSafeObservable())
                    .subscribe(p -> {
                        contactAddress.onNext(peer.getJid());
                        contactDisplayName.onNext(peer.getDisplayName());
                        contactAccountAddress.onNext(peer.getAccount().getJid());
                        contactAvatarBase.onNext(new Tuple<>(peer.getDisplayName(), peer.getJid()));
                    }));

            // Device keys
            addDisposable(
                    Observable.combineLatest(
                            openPgpRepository.observeFingerprints(peer.getAccount().getId(), peer.getJid())
                                    .doOnComplete(() -> LOGGER.log(Level.INFO, "Fingerprint complete"))
                                    .doOnNext(f -> LOGGER.log(Level.INFO, "Fingerprint Update: " + f)),
                            ikeyRepository.loadRecord(accountId, peer.getJid())
                                    .doOnComplete(() -> LOGGER.log(Level.INFO, "Record complete"))
                                    .doOnNext(r -> LOGGER.log(Level.INFO, "Record update: " + r)),
                            (viewItems, ikeyRecord) -> {
                                LOGGER.log(Level.INFO, "Combining");
                                for (FingerprintViewItem item : viewItems) {
                                    OpenPgpTrustStore.Trust trust = openPgpStore.getTrust(item.getOwner(), item.getFingerprint());
                                    item.setTrusted(trust);
                                }
                                return viewItems;
                            })
                            .doOnNext(m -> LOGGER.log(Level.INFO, "Combined Update: " + m))
                            .compose(schedulers.executeUiSafeObservable())
                            .doOnComplete(() -> LOGGER.log(Level.INFO, "Completed"))
                            .doOnDispose(() -> LOGGER.log(Level.INFO, "Disposed"))
                            .doOnTerminate(() -> LOGGER.log(Level.INFO, "Terminate"))
                            .subscribe(fingerprints -> contactDeviceFingerprints.onNext(fingerprints)));

            // Ikey
            addDisposable(ikeyRepository
                    .loadRecord(peer.getAccount().getId(), peer.getJid())
                    .compose(schedulers.executeUiSafeObservable())
                    .map(record -> new FingerprintViewItem(
                            peer.getAccount().getId(), record.getJid(),
                            new OpenPgpV4Fingerprint(record.getSuperordinate()),
                            record.getTimestamp(), record.getTimestamp(),
                            record.getTrust())
                    )
                    .map(Optional::new)
                    .subscribe(contactIdentityFingerprint::onNext));
        });
    }

    private void setupPresenceEventListener(Roster roster, EntityBareJid jid) {
        if (presenceEventListener != null) {
            if (presenceEventListener.getJid().equals(jid)) {
                // already set up
                return;
            }
            // outdated listener from previous contact
            roster.removePresenceEventListener(presenceEventListener);
        }

        presenceEventListener = new FilteredPresenceEventListener(jid) {
            @Override
            public void presenceReceived(Presence presence) {
                contactPresenceMode.onNext(presence.getMode());
                if (presence.getStatus() != null) {
                    contactPresenceStatus.onNext(presence.getStatus());
                }

                RosterEntry entry = roster.getEntry(jid);
                if (entry == null) {
                    return;
                }

                List<RosterGroup> groups = entry.getGroups();
                List<String> groupNames = new ArrayList<>(groups.size());
                for (RosterGroup group : groups) {
                    groupNames.add(group.getName());
                }
                contactGroups.onNext(groupNames);
            }
        };
        // Trigger once
        Presence presence = roster.getPresence(jid);
        if (presence != null) {
            presenceEventListener.presenceReceived(presence);
        }

        roster.addPresenceEventListener(presenceEventListener);
    }

    public Observable<EntityBareJid> getContactAddress() {
        return contactAddress;
    }

    public Observable<String> getContactDisplayName() {
        return contactDisplayName;
    }

    public Observable<EntityBareJid> getContactAccountAddress() {
        return contactAccountAddress;
    }

    public Observable<Presence.Mode> getContactPresenceMode() {
        return contactPresenceMode;
    }

    public Observable<String> getContactPresenceStatus() {
        return contactPresenceStatus;
    }

    public Observable<List<String>> getContactGroups() {
        return contactGroups;
    }

    public Observable<Optional<FingerprintViewItem>> getContactIdentityFingerprint() {
        return contactIdentityFingerprint;
    }

    public Observable<List<FingerprintViewItem>> getContactDeviceFingerprints() {
        return contactDeviceFingerprints;
    }

    public Observable<Tuple<String, EntityBareJid>> getContactAvatarBase() {
        return contactAvatarBase;
    }

    public Single<UUID> getOrCreateChat() {
        return peerRepository.getPeer(peerId)
                .flatMapSingle(directChatRepository::getOrCreateChatWithPeer)
                .map(Chat::getId);
    }

    public void changeContactName(String newName)
            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
            InterruptedException, SmackException.NoResponseException {
        if (!newName.trim().isEmpty()) {
            RosterEntry entry = roster.getEntry(contactAddress.getValue());
            entry.setName(newName);
        }
    }

    public void markDeviceFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, boolean checked) throws IOException {
        openPgpStore.setTrust(getContactAddress().blockingFirst(), fingerprint, checked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted);
    }

    public void markIkeyFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, EntityBareJid owner, boolean isChecked) {
        LOGGER.log(Level.INFO, "Marking Ikey " + fingerprint + " as " + (isChecked ? "trusted" : "untrusted"));
        OpenPgpTrustStore.Trust trust = isChecked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted;
        addDisposable(ikeyRepository.storeSuperordinateTrust(accountId, owner, fingerprint, trust)
                .subscribe(() -> LOGGER.log(Level.INFO, "Marked Ikey " + fingerprint + " of " + owner + " as " + trust),
                        e -> LOGGER.log(Level.SEVERE, "Error marking ikey " + fingerprint + " of " + owner + " as " + trust, e)));
    }

    public static abstract class FilteredPresenceEventListener extends CombinedPresenceListener {

        @Getter
        private final EntityBareJid jid;

        public FilteredPresenceEventListener(EntityBareJid jid) {
            this.jid = jid;
        }

        @Override
        public void presenceReceived(Jid address, Presence presence) {
            EntityBareJid entityBareJid = address.asEntityBareJidIfPossible();
            if (entityBareJid != null && entityBareJid.equals(jid)) {
                presenceReceived(presence);
            }
        }

        public abstract void presenceReceived(Presence presence);
    }

    @Override
    public void dispose() {
        LOGGER.log(Level.INFO, "Dispose");
        compositeDisposable.dispose();
        if (presenceEventListener != null) roster.removePresenceEventListener(presenceEventListener);
    }
}
