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

import org.jivesoftware.smack.util.StringUtils;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.account.error.PasswordError;
import org.mercury_im.messenger.core.account.error.UsernameError;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.connection.exception.InvalidCredentialsException;
import org.mercury_im.messenger.core.connection.exception.ServerUnreachableException;
import org.mercury_im.messenger.core.connection.state.ConnectionState;
import org.mercury_im.messenger.core.connection.state.ConnectivityState;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.entity.Account;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;

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

public class LoginViewModel implements MercuryViewModel {

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

    private final MercuryConnectionManager connectionManager;
    private final AccountRepository accountRepository;

    private final SchedulersFacade schedulers;

    private BehaviorSubject<Optional<UsernameError>> loginUsernameError;
    private BehaviorSubject<Optional<PasswordError>> loginPasswordError;
    private BehaviorSubject<Boolean> isLoginPossible;
    private BehaviorSubject<Optional<Account>> isLoginSuccessful;
    private BehaviorSubject<Boolean> displayProgressBar;

    private EntityBareJid loginUsernameValue;
    private String loginPasswordValue;
    private Account account;

    @Inject
    public LoginViewModel(MercuryConnectionManager connectionManager,
                          AccountRepository accountRepository,
                          SchedulersFacade schedulers) {
        this.connectionManager = connectionManager;
        this.accountRepository = accountRepository;
        this.schedulers = schedulers;

        reset();
    }

    public void reset() {
        loginUsernameError = BehaviorSubject.createDefault(new Optional<>());
        loginPasswordError = BehaviorSubject.createDefault(new Optional<>());
        isLoginPossible = BehaviorSubject.createDefault(false);
        isLoginSuccessful = BehaviorSubject.createDefault(new Optional<>());
        displayProgressBar = BehaviorSubject.createDefault(false);

        loginUsernameValue = null;
        loginPasswordValue = null;
    }

    public Observable<Optional<UsernameError>> getLoginUsernameError() {
        return loginUsernameError;
    }

    public Observable<Optional<PasswordError>> getLoginPasswordError() {
        return loginPasswordError;
    }

    public Observable<Boolean> isLoginPossible() {
        return isLoginPossible;
    }

    public Observable<Optional<Account>> isLoginSuccessful() {
        return isLoginSuccessful;
    }

    public synchronized void onLoginUsernameChanged(String username) {
        if (StringUtils.isNullOrEmpty(username)) {
            loginUsernameValue = null;
            loginUsernameError.onNext(new Optional<>(UsernameError.emptyUsername));
        } else {
            try {
                loginUsernameValue = JidCreate.entityBareFrom(username);
                loginUsernameError.onNext(new Optional<>());
            } catch (XmppStringprepException e) {
                loginUsernameValue = null;
                loginUsernameError.onNext(new Optional<>(UsernameError.invalidUsername));
            }
        }
        updateLoginPossible();
    }

    public synchronized void onLoginPasswordChanged(String password) {
        if (StringUtils.isNullOrEmpty(password)) {
            loginPasswordValue = null;
            loginPasswordError.onNext(new Optional<>(PasswordError.emptyPassword));
        } else {
            loginPasswordValue = password;
            loginPasswordError.onNext(new Optional<>());
        }
        updateLoginPossible();
    }

    private synchronized void updateLoginPossible() {
        isLoginPossible.onNext(loginUsernameValue != null && loginPasswordValue != null);
    }

    public synchronized Account login() {
        displayProgressBar.onNext(true);
        if (!isLoginPossible.getValue()) {
            // Prevent race condition where account would be logged in twice
            return account;
        }
        isLoginPossible.onNext(false);

        account = createAccountEntity();

        //MercuryConnection connection = connectionManager.createConnection(account);
        addDisposable(accountRepository.upsertAccount(account)
                //.andThen(connection.connect())
                //.andThen(connection.login())
                //.andThen(connectionManager.registerConnection(connection))
                .subscribeOn(schedulers.getNewThread())
                .observeOn(schedulers.getUiScheduler())
                .subscribe(
                        a -> LOGGER.log(Level.INFO, "Account " + a + " successfully inserted."),
                        this::onLoginFailed
                ));

        addDisposable(connectionManager.observeConnectionPool()
                .map(cp -> {
                    ConnectionState state = cp.getConnectionStates().get(account.getId());
                    if (state == null) {
                        return ConnectivityState.disconnected;
                    } else {
                        return state.getConnectivity();
                    }
                })
                .filter(s -> s == ConnectivityState.disconnectedOnError || s == ConnectivityState.authenticated)
                .firstOrError()
                .compose(schedulers.executeUiSafeSingle())
                .subscribe(connectivityState -> {
                    displayProgressBar.onNext(false);
                    if (connectivityState == ConnectivityState.disconnectedOnError) {
                        onLoginFailed(new Exception());
                    } else {
                        onLoginSuccessful(account);
                    }
                }));

        return account;
    }

    private Account createAccountEntity() {
        Account account = new Account();
        account.setAddress(loginUsernameValue.asEntityBareJidString());
        account.setPassword(loginPasswordValue);
        account.setEnabled(true);

        return account;
    }

    private void onLoginSuccessful(Account account) {
        LOGGER.log(Level.INFO, "Login successful.");
        isLoginSuccessful.onNext(new Optional<>(account));
    }

    private void onLoginFailed(Throwable error) {
        LOGGER.log(Level.INFO, "Login failed!");
        isLoginPossible.onNext(true);
        if (error instanceof InvalidCredentialsException) {
            loginPasswordError.onNext(new Optional<>(PasswordError.incorrectPassword));
        } else if (error instanceof ServerUnreachableException) {
            loginUsernameError.onNext(new Optional<>(UsernameError.unreachableServer));
        }
    }

    public Observable<Boolean> isDisplayProgressBar() {
        return displayProgressBar;
    }
}
