package org.mercury_im.messenger.xmpp;

import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.xmpp.exception.InvalidCredentialsException;
import org.mercury_im.messenger.xmpp.exception.ServerUnreachableException;
import org.mercury_im.messenger.xmpp.state.ConnectionState;
import org.mercury_im.messenger.xmpp.state.ConnectivityState;

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

import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
import lombok.Getter;

public class MercuryConnection {

    private static final Logger LOGGER = Logger.getLogger("MercuryConnection");

    @Getter
    private XMPPConnection connection;

    @Getter
    private final Account account;

    private final BehaviorSubject<ConnectionState> state;

    public MercuryConnection(XMPPConnection connection, Account account) {
        this.connection = connection;
        this.account = account;

        this.state = BehaviorSubject.createDefault(new ConnectionState(account.getId(), this,
                ConnectivityState.disconnected, false, false));
    }

    public UUID getAccountId() {
        return getAccount().getId();
    }

    public Observable<ConnectionState> observeConnection() {
        return state;
    }

    public MercuryConnection connect() throws ServerUnreachableException {
        if (getConnection().isConnected()) {
            return this;
        }
        doConnect();
        return this;
    }

    private MercuryConnection doConnect() throws ServerUnreachableException {
        AbstractXMPPConnection connection = (AbstractXMPPConnection) getConnection();
        try {
            connection.connect();
            return this;
        } catch (SmackException.EndpointConnectionException e) {
            connection.disconnect();
            throw new ServerUnreachableException("Cannot connect to server " + connection.getXMPPServiceDomain().asUnescapedString(), e);
        } catch (IOException | InterruptedException | XMPPException | SmackException e) {
            throw new AssertionError("Unexpected exception.", e);
        }
    }

    public void login() throws ServerUnreachableException, InvalidCredentialsException {
        if (getConnection().isAuthenticated()) {
            return;
        }
        doLogin();
    }

    private void doLogin() throws InvalidCredentialsException, ServerUnreachableException {
        try {
            connect();
            ((AbstractXMPPConnection) getConnection()).login();
        } catch (SASLErrorException e) {
            throw new InvalidCredentialsException("Credentials of account " + account.getId() + " are invalid.", e);
        } catch (InterruptedException | XMPPException | SmackException | IOException e) {
            throw new AssertionError("Unexpected exception.", e);
        }
    }

    private final ConnectionListener connectionListener = new ConnectionListener() {
        @Override
        public void connected(XMPPConnection connection) {
            state.onNext(state.getValue()
                    .withConnectivity(ConnectivityState.connected)
                    .withAuthenticated(false));
        }

        @Override
        public void authenticated(XMPPConnection connection, boolean resumed) {
            state.onNext(state.getValue()
                    .withConnectivity(ConnectivityState.connected)
                    .withAuthenticated(true)
                    .withResumed(resumed));
        }

        @Override
        public void connectionClosed() {
            state.onNext(state.getValue()
                    .withConnectivity(ConnectivityState.disconnected)
                    .withAuthenticated(false));
        }

        @Override
        public void connectionClosedOnError(Exception e) {
            state.onNext(state.getValue()
                    .withConnectivity(ConnectivityState.disconnected)
                    .withAuthenticated(false));
        }
    };

    public void shutdown() {
        if (connection.isConnected()) {
            ((AbstractXMPPConnection) getConnection()).disconnect();
        } else {
            ((AbstractXMPPConnection) getConnection()).instantShutdown();
        }
    }
}
