package org.mercury_im.messenger.core.centers;

import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smackx.caps.EntityCapsManager;
import org.jivesoftware.smackx.csi.ClientStateIndicationManager;
import org.jivesoftware.smackx.mam.MamManager;
import org.mercury_im.messenger.core.connection.MercuryConfiguration;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.xmpp.model.AccountModel;
import org.mercury_im.messenger.xmpp.model.ChatModel;
import org.mercury_im.messenger.xmpp.repository.RequeryAccountRepository;
import org.mercury_im.messenger.xmpp.repository.RosterRepository;
import org.mercury_im.messenger.core.stores.EntityCapsStore;
import org.mercury_im.messenger.core.stores.PlainMessageStore;
import org.mercury_im.messenger.core.stores.RosterStore;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

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

import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

@Singleton
public class ConnectionCenter {

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

    // Injected
    private final RequeryAccountRepository accountRepository;
    private final RosterRepository rosterRepository;
    private final PlainMessageStore messageStore;
    private final EntityCapsStore entityCapsStore;

    // Connections
    private final Map<Long, MercuryConnection> connectionMap =
            Collections.synchronizedMap(new HashMap<>());

    // Disposable for rx
    private final CompositeDisposable disposable = new CompositeDisposable();

    private final AtomicBoolean isConnectionCenterStarted = new AtomicBoolean(false);

    @Inject
    public ConnectionCenter(EntityCapsStore capsStore,
                            PlainMessageStore messageStore,
                            RequeryAccountRepository accountRepository,
                            RosterRepository rosterRepository) {
        LOGGER.log(Level.INFO, "ConnectionCenter initialized");
        this.entityCapsStore = capsStore;
        this.messageStore = messageStore;
        this.accountRepository = accountRepository;
        this.rosterRepository = rosterRepository;

        EntityCapsManager.setPersistentCache(capsStore);
        startUp();
    }

    /**
     * Start up the center by subscribing to changes of the {@link AccountModel accounts} in the
     * database. For each new {@link AccountModel} it creates a {@link MercuryConnection} and
     * stores it in the {@link #connectionMap}.
     */
    @SuppressWarnings("unchecked")
    public synchronized void startUp() {

        if (isConnectionCenterStarted.getAndSet(true)) {
            // already started.
            return;
        }

        // otherwise subscribe to accounts and create connections.
        Disposable allAccounts = accountRepository.getAll()
                .observeOn(Schedulers.newThread())
                .subscribe(accounts -> {
                    LOGGER.log(Level.INFO, "Accounts changed.");
                    Set<Long> accountIds = new HashSet<>();

                    // Add missing connections to the map
                    for (AccountModel account : accounts.toList()) {
                        accountIds.add(account.getId());
                        if (connectionMap.get(account.getId()) != null) {
                            continue;
                        }

                        LOGGER.log(Level.INFO, "Add new connection " + account.getJid().toString() + " to ConnectionCenter list.");
                        MercuryConnection connection = createConnection(account);
                        connectionMap.put(account.getId(), connection);

                        // initialize new connection
                        initializeConnection(connection);
                    }

                    // Remove unwanted connections from the map
                    for (long connectionId : connectionMap.keySet()) {
                        if (!accountIds.contains(connectionId)) {
                            LOGGER.log(Level.INFO, "Connection " + connectionId + " was deleted.");
                            AbstractXMPPConnection con =
                                    (AbstractXMPPConnection) connectionMap.get(connectionId).getConnection();
                            con.disconnect();
                            connectionMap.remove(connectionId);
                        }
                    }

                    for (AccountModel account : accounts) {
                        MercuryConnection connection = connectionMap.get(account.getId());
                        if (account.isEnabled()) {
                            if (connection.getConnection().isConnected()) {
                                continue;
                            }
                            LOGGER.log(Level.INFO, "Connecting connection " + account.getId() + " (" + account.getJid().toString() + ")");
                            connection.connect();
                            LOGGER.log(Level.INFO, "Connected!");
                        } else {
                            if (!connection.getConnection().isConnected()) {
                                continue;
                            }
                            LOGGER.log(Level.INFO, "Account " + account.getJid() + " (" + account.getJid().toString() + ") not enabled. Disable.");
                            connection.disconnect();
                        }
                    }
                });

        disposable.add(allAccounts);
    }

    public MercuryConnection getConnection(AccountModel account) {
        return getConnection(account.getId());
    }

    public MercuryConnection getConnection(long accountId) {
        return connectionMap.get(accountId);
    }

    public void putConnection(MercuryConnection connection) {
        connectionMap.put(connection.getAccountId(), connection);
    }

    /**
     * Create a new {@link MercuryConnection} with an underlying {@link XMPPTCPConnection}
     * from the credentials of an {@link AccountModel}.
     * The new connection will not be connected.
     *
     * @param accountModel accountModel
     *
     * @return disconnected mercury connection
     */
    public MercuryConnection createConnection(AccountModel accountModel) {
        LOGGER.log(Level.INFO, "Create Connection for " + accountModel.getJid().toString());
        XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder()
                .setHost(accountModel.getJid().getDomain().toString())
                .setXmppAddressAndPassword(accountModel.getJid(), accountModel.getPassword())
                .setConnectTimeout(2 * 60 * 1000)
                .setEnabledSSLCiphers(MercuryConfiguration.enabledCiphers)
                .setEnabledSSLProtocols(MercuryConfiguration.enabledProtocols)
                .build();

        AbstractXMPPConnection tcpConnection = new XMPPTCPConnection(configuration);
        return new MercuryConnection(tcpConnection, accountModel.getId());
    }

    public void initializeConnection(MercuryConnection connection) {
        // Register roster store
        RosterStore rosterStore = new RosterStore(rosterRepository, accountRepository);
        rosterStore.setAccountId(connection.getAccountId());
        rosterStore.subscribe();
        connection.getRoster().setRosterStore(rosterStore);

        // Register message store
        messageStore.registerForMercuryConnection(connection);
    }

    /**
     * Set Client State Indication status to active.
     *
     * @see <a href="https://xmpp.org/extensions/xep-0352.html">XEP-0352: Client State Indication</a>
     */
    public void clientStateActive() {
        LOGGER.log(Level.INFO, "CSI: App is going to foreground -> active");
        for (MercuryConnection mercuryConnection : connectionMap.values()) {
            XMPPConnection connection = mercuryConnection.getConnection();
            if (connection.isConnected() && ClientStateIndicationManager.isSupported(connection)) {
                try {
                    ClientStateIndicationManager.active(mercuryConnection.getConnection());
                } catch (SmackException.NotConnectedException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Set Client State Indication status to inactive.
     *
     * @see <a href="https://xmpp.org/extensions/xep-0352.html">XEP-0352: Client State Indication</a>
     */
    public void clientStateInactive() {
        LOGGER.log(Level.INFO, "CSI: App is going to background -> inactive");
        for (MercuryConnection mercuryConnection : connectionMap.values()) {
            XMPPConnection connection = mercuryConnection.getConnection();
            if (connection.isConnected() && ClientStateIndicationManager.isSupported(connection)) {
                try {
                    ClientStateIndicationManager.inactive(connection);
                } catch (SmackException.NotConnectedException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void requestMamMessagesFor(ChatModel chat) {

        MercuryConnection connection = connectionMap.get(chat.getPeer().getAccount().getId());
        if (connection == null) return;
        MamManager mamManager = MamManager.getInstanceFor(connection.getConnection());
        MamManager.MamQuery query;
        //if (chat.getEarliestMamMessageId() == null) {
        try {
            query = mamManager.queryMostRecentPage(chat.getPeer().getJid(), 100);
            messageStore.handleMamResult(chat.getPeer().getAccount().getId(), chat.getPeer().getJid(), query);
        } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NotLoggedInException | InterruptedException e) {
            e.printStackTrace();
        }
        //} else {
        //MamManager.MamQueryArgs queryArgs = MamManager.MamQueryArgs.builder()
        //        .beforeUid()
        //        .build();
        //query = mamManager.queryArchive()
        //}
    }
}
