package org.mercury_im.messenger.xmpp;

import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ReconnectionListener;
import org.jivesoftware.smack.ReconnectionManager;
import org.jivesoftware.smack.sm.packet.StreamManagement;
import org.jivesoftware.smackx.caps.EntityCapsManager;
import org.jivesoftware.smackx.iqversion.VersionManager;
import org.jivesoftware.smackx.receipts.DeliveryReceiptManager;
import org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.data.repository.Repositories;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.store.MercuryEntityCapsStore;
import org.mercury_im.messenger.usecase.LogIntoAccount;
import org.mercury_im.messenger.usecase.RosterStoreBinder;
import org.mercury_im.messenger.util.Optional;
import org.mercury_im.messenger.xmpp.state.ConnectionPoolState;
import org.mercury_im.messenger.xmpp.state.ConnectionState;

import java.util.ArrayList;
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 javax.inject.Singleton;

import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;

@Singleton
public class MercuryConnectionManager {

    private static final Logger LOGGER = Logger.getLogger("ConnectionManager");
    private static final XmppConnectionFactory connectionFactory = new XmppConnectionFactory();

    private final Map<UUID, MercuryConnection> connections = new ConcurrentHashMap<>();
    private final Map<UUID, Disposable> connectionDisposables = new ConcurrentHashMap<>();

    private final BehaviorSubject<ConnectionPoolState> stateObservable =
            BehaviorSubject.createDefault(new ConnectionPoolState());

    private final AccountRepository accountRepository;

    private final RosterStoreBinder rosterStoreBinder;
    private final MercuryEntityCapsStore entityCapsStore;

    private final CompositeDisposable disposable = new CompositeDisposable();

    static {
        ReconnectionManager.setEnabledPerDefault(true);
        ReconnectionManager.setDefaultReconnectionPolicy(ReconnectionManager.ReconnectionPolicy.RANDOM_INCREASING_DELAY);

        VersionManager.setAutoAppendSmackVersion(false);
        VersionManager.setDefaultVersion("Mercury-IM", "0.0.1-little-joe");

        DeliveryReceiptManager.setDefaultAutoReceiptMode(DeliveryReceiptManager.AutoReceiptMode.ifIsSubscribed);

        StableUniqueStanzaIdManager.setEnabledByDefault(true);
    }

    @Inject
    public MercuryConnectionManager(Repositories repositories,
                                    RosterStoreBinder rosterStoreBinder,
                                    MercuryEntityCapsStore entityCapsStore) {
        this.accountRepository = repositories.getAccountRepository();
        this.rosterStoreBinder = rosterStoreBinder;
        this.entityCapsStore = entityCapsStore;

        EntityCapsManager.setPersistentCache(entityCapsStore);
    }

    public List<MercuryConnection> getConnections() {
        return new ArrayList<>(connections.values());
    }

    public Observable<ConnectionPoolState> observeConnectionPool() {
        return stateObservable;
    }


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

    public MercuryConnection getConnection(UUID id) {
        return connections.get(id);
    }

    public MercuryConnection createConnection(Account account) {
        return new MercuryConnection(connectionFactory.createConnection(account), account);
    }

    public void registerConnections(List<Account> accounts) {
        for (Account account : accounts) {
            if (!connections.containsKey(account.getId())) {
                MercuryConnection connection = createConnection(account);
                registerConnection(connection);
            }
        }
    }

    public void registerConnection(MercuryConnection connection) {
        LOGGER.log(Level.INFO, "Register Connection " + connection.getAccountId());
        putConnection(connection);
        disposable.add(accountRepository
                .observeAccount(connection.getAccountId())
                .subscribeOn(Schedulers.newThread())
                .observeOn(Schedulers.newThread())
                .subscribe(event ->
                        handleOptionalAccountChangedEvent(connection, event)));
    }

    private void putConnection(MercuryConnection connection) {
        connections.put(connection.getAccountId(), connection);
        connectionDisposables.put(connection.getAccountId(), connection.observeConnection().subscribe(s ->
                stateObservable.onNext(updatePoolState(stateObservable.getValue(), s))));
        bindConnection(connection);
    }

    private ConnectionPoolState updatePoolState(ConnectionPoolState poolState, ConnectionState conState) {
        Map<UUID, ConnectionState> states = poolState.getConnectionStates();
        states.put(conState.getId(), conState);
        return new ConnectionPoolState(states);
    }

    public void bindConnection(MercuryConnection connection) {
        rosterStoreBinder.setRosterStoreOn(connection);
        ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection.getConnection())
                .addReconnectionListener(new ReconnectionListener() {
                    @Override
                    public void reconnectingIn(int seconds) {
                        LOGGER.log(Level.FINER, "Reconnecting connection " + connection.getAccountId() + " in " + seconds + " seconds.");
                    }

                    @Override
                    public void reconnectionFailed(Exception e) {
                        LOGGER.log(Level.WARNING, "Reconnection of connection " + connection.getAccountId() + " failed.", e);
                    }
                });
    }

    private void handleOptionalAccountChangedEvent(MercuryConnection connection, Optional<Account> event) {
        if (event.isPresent()) {
            handleAccountChangedEvent(connection, event.getItem());
        } else {
            handleAccountRemoved(connection);
        }
    }

    private void handleAccountChangedEvent(MercuryConnection connection, Account account) {
        if (account.isEnabled()) {
            handleAccountEnabled(connection);
        } else {
            handleAccountDisabled(connection);
        }
    }

    private void handleAccountDisabled(MercuryConnection connection) {
        LOGGER.log(Level.FINER, "HandleAccountDisabled: " + connection.getAccountId());
        connectionDisconnect(connection);
    }

    private void handleAccountEnabled(MercuryConnection connection) {
        LOGGER.log(Level.FINER, "HandleAccountEnabled: " + connection.getAccountId());
        connectionLogin(connection);
    }

    private void connectionLogin(MercuryConnection connection) {
        disposable.add(LogIntoAccount.with(connection).executeAndPossiblyThrow()
                .subscribeOn(Schedulers.newThread())
                .subscribe(() -> LOGGER.log(Level.FINER, "Logged in."),
                        error -> LOGGER.log(Level.SEVERE, "Connection error!", error)));
    }

    private void handleAccountRemoved(MercuryConnection connection) {
        disconnectAndRemoveConnection(connection);
    }

    private void disconnectAndRemoveConnection(MercuryConnection connection) {
        connectionDisconnect(connection);
        removeConnection(connection);
    }

    private void connectionDisconnect(MercuryConnection connection) {
        ((AbstractXMPPConnection) connection.getConnection()).disconnect();
    }

    private void removeConnection(MercuryConnection connection) {
        LOGGER.log(Level.FINER, "Remove Connection: " + connection.getAccountId());
        connections.remove(connection.getAccountId());
        connectionDisposables.remove(connection.getAccountId()).dispose();
        stateObservable.onNext(updatePoolState(stateObservable.getValue()));
    }

    private ConnectionPoolState updatePoolState(ConnectionPoolState value) {
        Map<UUID, ConnectionState> states = value.getConnectionStates();
        for (UUID id : connections.keySet()) {
            if (!states.containsKey(id)) {
                states.remove(id);
            }
        }
        return new ConnectionPoolState(states);
    }

}
