package org.mercury_im.messenger.core;

import org.bouncycastle.openpgp.PGPException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox_im.OXInstantMessagingManager;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.connection.message.MessageConsignor;
import org.mercury_im.messenger.core.connection.message.OxMessageConsignor;
import org.mercury_im.messenger.core.connection.message.PlaintextMessageConsignor;
import org.mercury_im.messenger.core.data.repository.MessageRepository;
import org.mercury_im.messenger.core.exception.ConnectionNotFoundException;
import org.mercury_im.messenger.core.exception.ContactAlreadyAddedException;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.contact.Peer;

import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;
import javax.inject.Singleton;

import io.reactivex.Completable;

@Singleton
public class Messenger {

    public static final String TAG = "MercuryIM";
    private static final Logger LOGGER = Logger.getLogger(Messenger.class.getName());

    private final MercuryConnectionManager connectionManager;
    private final MessageRepository messageRepository;
    private final SchedulersFacade schedulers;

    @Inject
    public Messenger(MercuryConnectionManager connectionManager,
                     MessageRepository messageRepository,
                     SchedulersFacade schedulers) {
        this.connectionManager = connectionManager;
        this.messageRepository = messageRepository;
        this.schedulers = schedulers;
    }

    public MercuryConnectionManager getConnectionManager() {
        return connectionManager;
    }

    public Completable addContact(UUID accountId, String contactAddress) {
        return Completable.fromAction(() -> doAddContact(accountId, contactAddress));
    }

    private void doAddContact(UUID accountId, String contactAddress)
            throws ConnectionNotFoundException, XmppStringprepException, ContactAlreadyAddedException,
            SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
            SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
        MercuryConnection connection = getConnection(accountId);

        EntityBareJid jid = JidCreate.entityBareFrom(contactAddress);
        Roster roster = Roster.getInstanceFor(connection.getConnection());
        if (roster.getEntry(jid) != null) {
            throw new ContactAlreadyAddedException(jid);
        }

        if (roster.isSubscriptionPreApprovalSupported()) {
            LOGGER.log(Level.INFO, "Pre-Approval supported.");
            try {
                roster.preApproveAndCreateEntry(jid, null, null);
            } catch (SmackException.FeatureNotSupportedException e) {
                throw new AssertionError("pre-approval failed even though the feature is announced.");
            }
        } else {
            roster.createItemAndRequestSubscription(jid, null, null);
        }
    }

    public Completable deleteContact(Peer contact) {
        return Completable.fromAction(() -> doDeleteContact(contact));
    }

    private void doDeleteContact(Peer contact)
            throws ConnectionNotFoundException, XmppStringprepException,
            SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
            SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
        MercuryConnection connection = getConnection(contact.getAccount().getId());

        Roster roster = Roster.getInstanceFor(connection.getConnection());
        EntityBareJid jid = JidCreate.entityBareFrom(contact.getAddress());
        RosterEntry entry = roster.getEntry(jid);
        if (entry != null) {
            roster.removeEntry(entry);
        } else {
            throw new IllegalStateException("Contact " + jid.toString() + " not in roster!");
        }
    }

    public Completable sendMessage(Peer peer, String body) {
        return Completable.fromAction(() -> doSendMessage(peer, body));
    }

    private void doSendMessage(Peer contact, String body) throws ConnectionNotFoundException,
            SmackException.NotConnectedException, InterruptedException {
        MercuryConnection connection = getConnection(contact.getAccount().getId());
        ChatManager chatManager = ChatManager.getInstanceFor(connection.getConnection());
        chatManager.chatWith(JidCreate.entityBareFromOrThrowUnchecked(contact.getAddress()))
                .send(body);
    }

    public Completable sendEncryptedMessage(Peer contact, String body) {
        return Completable.fromAction(() -> doSendEncryptedMessage(contact, body));
    }

    public void doSendEncryptedMessage(Peer contact, String body) throws ConnectionNotFoundException,
            InterruptedException, PGPException, SmackException.NotConnectedException,
            SmackException.NotLoggedInException, IOException {
        MercuryConnection connection = getConnection(contact.getAccount().getId());
        OpenPgpManager oxManager = OpenPgpManager.getInstanceFor(connection.getConnection());
        OXInstantMessagingManager oximManager = OXInstantMessagingManager.getInstanceFor(connection.getConnection());
        oximManager.sendOxMessage(oxManager.getOpenPgpContact(contact.getJid()), body);
    }

    private MercuryConnection getConnection(UUID accountId) throws ConnectionNotFoundException {
        MercuryConnection connection = getConnectionManager().getConnection(accountId);
        if (connection == null) {
            throw new ConnectionNotFoundException(accountId);
        }
        return connection;
    }

    public MessageConsignor getMessageConsignor(Chat chat) {
        switch (chat.getChatPreferences().getEncryption()) {
            case plain:
                return new PlaintextMessageConsignor(connectionManager, messageRepository, chat);
            case ox_sign:
            case ox_crypt:
            case ox_signcrypt:
                return new OxMessageConsignor(connectionManager, messageRepository, chat);
        }
        throw new AssertionError("Illegal Encryption Type: " + chat.getChatPreferences().getEncryption());
    }
}
