package com.getbubblenow.android.repository;

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.util.Log;
import android.widget.Toast;

import com.getbubblenow.android.Application;
import com.getbubblenow.android.R;
import com.getbubblenow.android.api.enums.MFAuthType;
import com.getbubblenow.android.model.LoginErrBody;
import com.getbubblenow.android.model.Network;
import com.getbubblenow.android.model.NetworkStatus;
import com.getbubblenow.android.model.ObservableTunnel;
import com.getbubblenow.android.activity.MainActivity;
import com.getbubblenow.android.api.ApiConstants;
import com.getbubblenow.android.api.network.ClientApi;
import com.getbubblenow.android.api.network.ClientService;
import com.getbubblenow.android.api.network.NetworkBoundStatusResource;
import com.getbubblenow.android.model.User;
import com.getbubblenow.android.model.UserErrorBody;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.Tunnel;
import com.wireguard.android.backend.Tunnel.State;
import com.getbubblenow.android.model.Device;
import com.getbubblenow.android.resource.StatusResource;
import com.getbubblenow.android.util.TunnelStore;
import com.getbubblenow.android.util.UserStore;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Predicate;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subscribers.DisposableSubscriber;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.Buffer;
import retrofit2.Call;
import retrofit2.HttpException;
import retrofit2.Response;

public final class DataRepository {

    private static final String TAG = "DataRepository";

    private static final int DELAY_VALUE = 10;
    private static final int PROGRESS_DELAY_VALUE = 5;
    private static final int NET_UNAVAILABLE_DELAY_VALUE = 10;
    private static final int HTTP_422_ERR_CODE = 422;
    private static final int HTTP_404_ERR_CODE = 404;
    private static final int HTTP_500_ERR_CODE = 500;
    private static final int FULL_PROGRESS = 100;
    private static final Long ZERO_PROGRESS = 0L;

    private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
    private static final int ANDROID_ID = 1;

    private static final String SEPARATOR = ":";
    private static final String SPACE = " ";
    private static final String DELIMITER = "\\A";
    private static final String HOST_DELIMITER = ".";

    private static final String TUNNEL_NAME = "Bubble";

    private static final String NO_INTERNET_CONNECTION = "no internet connection";
    private static final String LOGIN_FAILED = "Login Failed";
    private static final String AUTH_FAILED = "Authentication Failed";
    private static final String SAGE_LIST_IS_EMPTY = "Sage list is empty";

    private static final String URL_SUFFIX = "/api/";
    private static final String BASE_URL_PREFIX = "https://";
    private static final String BASE_URL_SUFFIX = ":1443/api/";

    private static volatile DataRepository instance;

    private String baseUrl = "";
    private Disposable bubbleCheckDisposable;
    private Disposable progressDisposable;
    private String deviceName;
    private String deviceID;

    private ClientApi clientApi;
    private final CompositeDisposable compositeDisposable;
    private ObservableTunnel pendingTunnel;

    private List<String> nodes;
    private String hostname = "";
    private boolean stopNetworkStatusLiveData = false;
    private final MutableLiveData<StatusResource<Network>> networkStatusLiveData = new MutableLiveData<>();
    private final MutableLiveData<Long> progressLiveData = new MutableLiveData<>();

    private DataRepository(final Context context, final String url) {
        baseUrl = url;
        clientApi = ClientService.getInstance().createClientApi(url);
        compositeDisposable = new CompositeDisposable();
    }

    public static void buildRepositoryInstance(final Context context, final String url) {
        if (instance == null) {
            synchronized (DataRepository.class) {
                if (instance == null) {
                    instance = new DataRepository(context, url);
                }
            }
        }
    }

    public void buildClientService(final String url) {
        baseUrl = url;
        clientApi = ClientService.getInstance().createClientApi(url);
    }

    public static DataRepository getRepositoryInstance() {
        return instance;
    }

    public void disposeBubbleCheck() {
        if (bubbleCheckDisposable == null) {
            return;
        }
        if (!bubbleCheckDisposable.isDisposed()) {
            bubbleCheckDisposable.dispose();
        }
    }

    public void disposeProgress() {
        if (progressDisposable == null) {
            return;
        }
        if (!progressDisposable.isDisposed()) {
            progressDisposable.dispose();
        }
    }

    public MutableLiveData<StatusResource<List<Network>>> checkBubbleOnce(final Context context) {
        final MutableLiveData<StatusResource<List<Network>>> data = new MutableLiveData<>();
        final HashMap<String, String> header = new HashMap<>();
        header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken());
        clientApi.getNodeBaseURI(header)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DisposableSingleObserver<List<Network>>() {

                    @Override public void onSuccess(final List<Network> networks) {
                        data.postValue(StatusResource.success(getAliveBubbles(networks)));
                    }

                    @Override public void onError(final Throwable throwable) {
                        setErrorMessage(throwable, data);
                        Log.e(TAG, "Failed to acquire list of bubble networks from the remote server. "
                                + throwable.getMessage(), throwable);
                    }
                });
        return data;
    }

    public MutableLiveData<StatusResource<List<Network>>> checkBubbleContinuously(final Context context) {
        final MutableLiveData<StatusResource<List<Network>>> data = new MutableLiveData<>();
        final HashMap<String, String> header = new HashMap<>();
        header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken());
        bubbleCheckDisposable = clientApi.getNodeBaseURI(header)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .repeatWhen(success -> success.delay(DELAY_VALUE, TimeUnit.SECONDS))
                .retryWhen(error -> error.delay(NET_UNAVAILABLE_DELAY_VALUE, TimeUnit.SECONDS))
                .subscribeWith(new DisposableSubscriber<List<Network>>() {

                    @Override public void onNext(final List<Network> networks) {
                        final List<Network> alive = getAliveBubbles(networks);
                        if (!alive.isEmpty()) {
                            data.postValue(StatusResource.success(alive));
                            onComplete();
                        }
                    }

                    @Override public void onError(final Throwable throwable) {
                        Log.e(TAG, "Failed to acquire list of bubble networks from the remote server. "
                                + throwable.getMessage(), throwable);
                    }

                    @Override public void onComplete() {
                        dispose();
                    }
                });
        return data;
    }

    private static List<Network> getAliveBubbles(final List<Network> networkList) {
        final Iterator<Network> iterator = networkList.iterator();
        while(iterator.hasNext()) {
            final Network entry = iterator.next();
            if (!(entry.isRunning() || entry.isStarting() || entry.isRestoring())) {
                iterator.remove();
            }
        }
        return networkList;
    }


    private void getNodeIndex(
            final int index,
            final String username,
            final String password,
            final Context context,
            final NetworkBoundStatusResource<byte[]> liveData
    ) {
        if (index < nodes.size()) {
            getNetwork(index, username, password, context, liveData);
        } else {
            liveData.postMutableLiveData(StatusResource.error(LOGIN_FAILED));
        }
    }

    public MutableLiveData<StatusResource<Network>> getNetworkStatusLiveData() {
        return networkStatusLiveData;
    }

    public MutableLiveData<Long> getProgressLiveData() {
        return progressLiveData;
    }

    private void getNetwork(final int nodeIdx,
                            final String username,
                            final String password,
                            final Context context,
                            final NetworkBoundStatusResource<byte[]> liveData) {

        final String node = nodes.get(nodeIdx);
        final HashMap<String, String> data = new HashMap<>();
        data.put(ApiConstants.USERNAME, username);
        data.put(ApiConstants.PASSWORD, password);
        buildClientService(node + URL_SUFFIX);
        final Disposable disposableLogin = clientApi.login(data)
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(user -> {
                    CredentialRepository.getInstance(context).setUsername(username);
                    CredentialRepository.getInstance(context).setSagePassword(password);
                    UserStore.getInstance(context).setSageHostname(node);

                    final String multifactorAuth = user.getMultifactorAuth();
                    if (multifactorAuth != null && !multifactorAuth.isEmpty()) {
                        liveData.postMutableLiveData(StatusResource.auth(getMultifactorAuthType(multifactorAuth)));
                    } else {
                        UserStore.getInstance(context).setSageToken(user.getToken());
                        UserStore.getInstance(context).setSageURL(node + URL_SUFFIX);
                        baseUrl = node + URL_SUFFIX;
                        buildClientService(baseUrl);
                        liveData.postMutableLiveData(StatusResource.success(null));
                    }
                }, throwable -> {
                    if (isTotpAuthenticationRequired(throwable)) {
                        // save credentials and host for further TOTP token check
                        CredentialRepository.getInstance(context).setUsername(username);
                        CredentialRepository.getInstance(context).setSagePassword(password);
                        UserStore.getInstance(context).setSageHostname(node);
                        liveData.postMutableLiveData(StatusResource.auth(MFAuthType.TOTP));
                    } else {
                        getNodeIndex((nodeIdx + 1), username, password, context, liveData);
                        Log.e("ERR", "getSages-login : " + node);
                    }
                });
        compositeDisposable.add(disposableLogin);
    }

    private static boolean isTotpAuthenticationRequired(final Throwable throwable) {
        if (throwable instanceof HttpException) {
            final Response<?> response =((HttpException) throwable).response();
            if (response != null) {
                if (response.code() == HTTP_422_ERR_CODE) {
                    try (final ResponseBody responseBody = response.errorBody()) {
                        if (responseBody != null) {
                            final UserErrorBody[] errorBody = ClientService.getInstance()
                                    .getErrorBodyAs(responseBody, UserErrorBody[].class);
                            if (errorBody.length > 0) {
                                if (errorBody[0].isTotpRequired() || errorBody[0].isTotpInvalid()) {
                                    return true;
                                }
                            }
                        }
                    } catch (final IOException ex) {
                        Log.e("ERR", "Cannot deserialize error body while checking error for totp token", ex);
                    }
                }
            }
        }
        return false;
    }


    private static boolean isNodeCredentialsRequired(final Throwable throwable) {
        if (throwable instanceof HttpException) {
            final Response<?> response =((HttpException) throwable).response();
            if (response != null) {
                return response.code() == HTTP_404_ERR_CODE;
            }
        }
        return false;
    }

    private static String getResourceFromErrorBody(final Throwable throwable) {
        if (throwable instanceof HttpException) {
            final Response<?> response =((HttpException) throwable).response();
            if (response != null) {
                try (final ResponseBody responseBody = response.errorBody()) {
                    if (responseBody != null) {
                        final LoginErrBody errorBody = ClientService.getInstance()
                                .getErrorBodyAs(responseBody, LoginErrBody.class);
                        return errorBody.getResource();
                    }
                } catch (final IOException ex) {
                    Log.e("ERR", "Cannot deserialize error body while checking error for node credentials", ex);
                }
            }
        }
        return null;
    }

    public MutableLiveData<StatusResource<byte[]>> loginToNode(final Context context, final String uuid) {
        stopNetworkStatusLiveData = false;
        return new NetworkBoundStatusResource<byte[]>() {

            @Override protected void createCall() {
                final NetworkBoundStatusResource<byte[]> liveData = this;
                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken());
                buildClientService(UserStore.getInstance(context).getSageURL());

                final Disposable getNodeBaseURIDisposable = clientApi.getNodeBaseURI(header)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .subscribe(networks -> {
                            if (networks.isEmpty()) {
                                postMutableLiveData(StatusResource.empty());
                                return;
                            }
                            final Network network = getNetworkByUuid(uuid, networks);
                            if (network == null) {
                                postMutableLiveData(StatusResource.empty());
                                return;
                            }
                            if (network.isRunning()) {
                                loginToNetwork(network);
                            } else if (network.isStarting() || network.isRestoring()) {
                                commitProgress(network);
                            } else {
                                postMutableLiveData(StatusResource.error(
                                        String.format("Bubble invalid state : %s.", network.getState()))
                                );
                            }
                        }, throwable -> {
                            final String message = throwable.getMessage();
                            if (message != null) {
                                Log.e("ERR", message);
                            }
                            setErrorMessage(throwable, liveData.getMutableLiveData());
                        });
                compositeDisposable.add(getNodeBaseURIDisposable);
            }

            private void commitProgress(final Network network) {
                networkStatusLiveData.postValue(StatusResource.success(network));

                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken());
                progressDisposable = clientApi.getNetworkState(
                        CredentialRepository.getInstance(context).getUsername(), network.getUuid(), header
                )
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .repeatWhen(completed -> completed.delay(PROGRESS_DELAY_VALUE, TimeUnit.SECONDS))
                        .takeUntil(networkStatusList -> stopNetworkStatusLiveData)
                        .subscribe(networkStatusList -> {
                            final NetworkStatus networkStatus = getNetworkStatus(networkStatusList);
                            if (networkStatus == null || networkStatus.getMessageKey().contains("error")) {
                                progressLiveData.postValue(ZERO_PROGRESS);
                                networkStatusLiveData.postValue(StatusResource.empty());
                                stopNetworkStatusLiveData = true;
                                return;
                            }
                            final long percent = networkStatus.getPercent();
                            progressLiveData.postValue(percent);
                            if (percent == FULL_PROGRESS) {
                                loginToNetworkWithCheck();
                                stopNetworkStatusLiveData = true;
                            }
                        }, throwable -> {
                            Log.e("ERR", "getNetworkState");
                            stopNetworkStatusLiveData = true;
                            setErrorMessage(throwable, getMutableLiveData());
                        });
            }

            private void loginToNetworkWithCheck() {
                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken());

                final Disposable getNodeBaseURIDisposable = clientApi.getNodeBaseURI(header)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .repeatWhen(completed -> completed.delay(DELAY_VALUE, TimeUnit.SECONDS))
                        .takeUntil((Predicate<? super List<Network>>) networkList -> getNetworkByUuid(uuid, networkList).isRunning())
                        .subscribe(networkList -> {
                            final Network network = getNetworkByUuid(uuid, networkList);
                            if (network == null) {
                                postMutableLiveData(StatusResource.empty());
                                return;
                            }
                            if (network.isRunning()) {
                                networkStatusLiveData.postValue(StatusResource.success(network));
                                loginToNetwork(network);
                            }
                        }, throwable -> {
                            Log.d("ERR", "getNodeBaseURI");
                            setErrorMessage(throwable, this.getMutableLiveData());
                        });
                compositeDisposable.add(getNodeBaseURIDisposable);
            }

            private NetworkStatus getNetworkStatus(final Iterable<NetworkStatus> networkStatusList) {
                NetworkStatus networkStatus = null;
                long ctime = 0;
                for (final NetworkStatus status : networkStatusList) {
                    if (uuid.equals(status.getNetwork())
                            && status.getCtime() > ctime) {
                        ctime = status.getCtime();
                        networkStatus = status;
                    }
                }
                return networkStatus;
            }

            private Network getNetworkByUuid(final String uuid, final Iterable<Network> networks) {
                for (final Network network : networks) {
                    if (uuid.equals(network.getUuid())) {
                        return network;
                    }
                }
                return null;
            }

            private void loginToNetwork(final Network network) {
                hostname = network.getName() + HOST_DELIMITER + network.getDomainName();
                UserStore.getInstance(context).setNetworkUuid(network.getUuid());
                UserStore.getInstance(context).setHostNameForAccount(hostname);
                baseUrl = BASE_URL_PREFIX + hostname + BASE_URL_SUFFIX;
                buildClientService(baseUrl);

                final String password = CredentialRepository.getInstance(context).getNodePassword().isEmpty() ?
                        CredentialRepository.getInstance(context).getSagePassword()
                        : CredentialRepository.getInstance(context).getNodePassword();

                login(CredentialRepository.getInstance(context).getUsername(), password, context, network, this);
            }

        }.getMutableLiveData();
    }


    public MutableLiveData<StatusResource<String>> getSignUpSage(final Context context) {
        return new NetworkBoundStatusResource<String>() {
            @Override protected void createCall() {
                buildClientService(ApiConstants.BOOTSTRAP_URL);
                final Disposable sagesDisposable = clientApi.getSages()
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(sages -> {
                            nodes = sages.getSages();
                            if (nodes != null && !nodes.isEmpty()) {
                                postMutableLiveData(StatusResource.success(nodes.get(0)));
                            } else {
                                Log.e("ERR", "There are no available sages at bootstrap URL.");
                                postMutableLiveData(StatusResource.error(SAGE_LIST_IS_EMPTY));
                            }
                        }, throwable -> {
                            Log.e("ERR", "Bootstrap URL cannot be reached.");
                            setErrorMessage(throwable, this.getMutableLiveData());
                        });
                compositeDisposable.add(sagesDisposable);
            }
        }.getMutableLiveData();
    }


    public MutableLiveData<StatusResource<byte[]>> loginToSage(final Context context, final String username, final String password) {
        return new NetworkBoundStatusResource<byte[]>() {

            @Override protected void createCall() {
                final String sageHostName = UserStore.getInstance(context).getSageHostname();
                if (!sageHostName.isEmpty()) {
                    nodes = Collections.singletonList(sageHostName);
                    getNodeIndex(0, username, password, context, this);
                    return;
                }

                final Disposable sagesDisposable = clientApi.getSages()
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(sages -> {
                            nodes = sages.getSages();
                            getNodeIndex(0, username, password, context, this);
                        }, throwable -> {
                            Log.d("ERR", "getSages");
                            setErrorMessage(throwable, this.getMutableLiveData());
                        });
                compositeDisposable.add(sagesDisposable);
            }
        }.getMutableLiveData();
    }

    public MutableLiveData<StatusResource<byte[]>> loginWithAuth(final Context context, final String mfaToken, final MFAuthType authType) {
        return new NetworkBoundStatusResource<byte[]>() {
            @Override protected void createCall() {
                final String username = CredentialRepository.getInstance(context).getUsername();
                final String password = CredentialRepository.getInstance(context).getSagePassword();
                final String sageHostname = UserStore.getInstance(context).getSageHostname();
                buildClientService(sageHostname + URL_SUFFIX);

                final Single<User> request;
                final HashMap<String, String> params = new HashMap<>();
                switch (authType) {
                    case TOTP:
                        params.put(ApiConstants.USERNAME, username);
                        params.put(ApiConstants.PASSWORD, password);
                        params.put(ApiConstants.TOTP_TOKEN, mfaToken);
                        request = clientApi.login(params);
                        break;
                    case EMAIL:
                    case SMS:
                        params.put(ApiConstants.NAME, "account");
                        params.put(ApiConstants.USERNAME, username);
                        request = clientApi.approve(mfaToken, params);
                        break;
                    default:
                        throw new IllegalStateException("Unexpected value: " + authType);
                }

                final Disposable disposableLogin = request
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(user -> {
                            UserStore.getInstance(context).setMfaToken(mfaToken);
                            final String multifactorAuth = user.getMultifactorAuth();
                            if (multifactorAuth != null && !multifactorAuth.isEmpty()) {
                                postMutableLiveData(StatusResource.auth(getMultifactorAuthType(multifactorAuth)));
                            } else {
                                UserStore.getInstance(context).setSageToken(user.getToken());
                                UserStore.getInstance(context).setSageURL(sageHostname + URL_SUFFIX);
                                baseUrl = sageHostname + URL_SUFFIX;
                                postMutableLiveData(StatusResource.success(null));
                            }
                        }, throwable -> {
                            if (isTotpAuthenticationRequired(throwable)) {
                                postMutableLiveData(StatusResource.auth(MFAuthType.TOTP));
                            } else {
                                postMutableLiveData(StatusResource.error(AUTH_FAILED));
                            }
                        });
                compositeDisposable.add(disposableLogin);
            }
        }.getMutableLiveData();
    }

    private MFAuthType getMultifactorAuthType(final String multifactorAuth) {
        // TODO: check authentication type : EMAIL, SMS, TOTP
        throw new UnsupportedOperationException("Additional MFA methods (email, sms) are not supported yet.");
    }


    private MutableLiveData<StatusResource<byte[]>> login(
            final String username,
            final String password,
            final Context context,
            final Network network,
            final NetworkBoundStatusResource<byte[]> liveData) {
        return new NetworkBoundStatusResource<byte[]>() {
            @Override protected void createCall() {
                HashMap<String, String> data = new HashMap<>();
                buildClientService(baseUrl);
                data.put(ApiConstants.USERNAME, username);
                data.put(ApiConstants.PASSWORD, password);

                final String mfaToken = UserStore.getInstance(context).getMfaTokenKey();
                if (!mfaToken.isEmpty()) {
                    data.put(ApiConstants.TOTP_TOKEN, mfaToken);
                }

                Disposable disposableLogin = clientApi.login(data)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .subscribe(user -> {
                            final String multifactorAuth = user.getMultifactorAuth();
                            if (multifactorAuth != null && !multifactorAuth.isEmpty()) {
                                liveData.postMutableLiveData(StatusResource.auth(getMultifactorAuthType(multifactorAuth)));
                            } else {
                                UserStore.getInstance(context).setToken(user.getToken());
                                if (!isDeviceLoggedIn(context)) {
                                    addDevice(context);
                                } else {
                                    getAllDevices(context);
                                }
                            }
                        }, throwable -> {
                            if (isTotpAuthenticationRequired(throwable)) {
                                liveData.postMutableLiveData(StatusResource.auth(MFAuthType.TOTP));
                            } else if (isNodeCredentialsRequired(throwable)) {
                                final String resource = getResourceFromErrorBody(throwable);
                                liveData.postMutableLiveData(StatusResource.auth(MFAuthType.NODE, resource));
                            } else {
                                Log.d("ERR", "login");
                                setErrorMessage(throwable, liveData.getMutableLiveData());
                            }
                        });
            }

            private void getAllDevices(final Context context) {
                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getToken());
                Disposable disposableAllDevices = clientApi.getAllDevices(header)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .subscribe(listDevices -> {
                            boolean hasDevice = false;
                            for (Device item : listDevices) {
                                //TODO Add errbit for this case
                                if (UserStore.getInstance(context).getDeviceID().equals(item.getUuid())) {
//                                    UserStore.getInstance(context).setToken(token);
                                    Log.i("INF", "SUCCESS NULL");
                                    setMutableLiveData(StatusResource.success(null));
                                    hasDevice = true;
                                    break;
                                }
                            }
                            if (!hasDevice) {
                                addDevice(context);
                            }
                        }, throwable -> {
                            Log.d("ERR", "getAllDevices");
                            setErrorMessage(throwable, liveData.getMutableLiveData());
                        });
                compositeDisposable.add(disposableAllDevices);
            }

            private void addDevice(final Context context) {
                final String brand = getBrand();
                final String model = getDeviceModel();
                final String imei = getDeviceID(context);
                final String deviceName = brand + SPACE + model + SPACE + SEPARATOR + SPACE + imei;
                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getToken());
                final Disposable disposableAllDevices = clientApi.getAllDevices(header)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                        .subscribe(listDevices -> {
                            String brandModel = brand + SPACE + model + SPACE;
                            final List<Device> list = listDevices;
                            final List<String> arrayListDevicesName = new ArrayList<>();
                            boolean hasDevice = false;
                            for (final Device device : list) {
                                final String[] deviceNameItem = device.getName().split(SEPARATOR);
                                final String[] myDeviceName = deviceName.split(SEPARATOR);
                                if (deviceNameItem.length > 1) {
                                    if (deviceNameItem[ANDROID_ID].equals(myDeviceName[ANDROID_ID])) {
                                        DataRepository.this.deviceName = device.getName();
                                        deviceID = device.getUuid();
                                        hasDevice = true;

                                        liveData.postMutableLiveData(StatusResource.success(null));
                                        networkStatusLiveData.postValue(StatusResource.success(network));

//                                        getConfig(context);
                                        break;
                                    } else {
                                        final String[] itemDevice = device.getName().split(SEPARATOR);
                                        if (itemDevice.length != 1) {
                                            if (itemDevice[0].contains(brandModel)) {
                                                arrayListDevicesName.add(itemDevice[0]);
                                            }
                                        }
                                    }

                                }
                            }
                            if (!hasDevice) {
                                if (arrayListDevicesName.isEmpty()) {
                                    brandModel = deviceName;
                                    final HashMap<String, String> body = new HashMap<>();
                                    body.put(ApiConstants.DEVICE_NAME, brandModel);
                                    body.put(ApiConstants.DEVICE_TYPE, "android");
                                    final Disposable disposableAddDevice = clientApi.addDevice(header, body)
                                            .subscribeOn(Schedulers.newThread())
                                            .observeOn(AndroidSchedulers.mainThread())
                                            .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                                            .subscribe(device -> {
                                                deviceID = device.getUuid();

                                                liveData.postMutableLiveData(StatusResource.success(null));
                                                networkStatusLiveData.postValue(StatusResource.success(network));

//                                                getConfig(context);
                                            }, throwable -> {
                                                setErrorMessage(throwable, liveData.getMutableLiveData());
                                                Log.d("ERR", "addDevice5");
                                                // setMutableLiveData(StatusResource.error(throwable.getMessage()));
                                            });
                                    compositeDisposable.add(disposableAddDevice);
                                } else {
                                    for (int i = (arrayListDevicesName.size() - 1); i >= arrayListDevicesName.size() - 1; i--) {
                                        if (arrayListDevicesName.get(i).contains(brandModel)) {
                                            final char[] arr = arrayListDevicesName.get(i).toCharArray();
                                            if (arr[arr.length - 2] != ')') {
                                                brandModel += "(2)" + SPACE + SEPARATOR + SPACE + imei;
                                            } else {
                                                String countDevice = "";
                                                int indexfirst = 0;
                                                int indexlast = 0;
                                                for (int j = arr.length - 1; j >= 0; j--) {
                                                    if (arr[j] == '(') {
                                                        indexfirst = j;
                                                    }
                                                    if (arr[j] == ')') {
                                                        indexlast = j;
                                                    }
                                                }
                                                final String device = new String(arr);
                                                countDevice += device.substring(indexfirst + 1, indexlast);
                                                int count = Integer.parseInt(countDevice);
                                                count++;
                                                brandModel += "(" + count + ")" + SPACE + SEPARATOR + SPACE + imei;
                                            }
                                        }
                                        final HashMap<String, String> body = new HashMap<>();
                                        body.put(ApiConstants.DEVICE_NAME, brandModel);
                                        body.put(ApiConstants.DEVICE_TYPE, "android");
                                        Disposable disposableAddDevice = clientApi.addDevice(header, body)
                                                .subscribeOn(Schedulers.newThread())
                                                .observeOn(AndroidSchedulers.mainThread())
                                                .retry((attempts, error) -> attempts < 5 && error instanceof IOException)
                                                .subscribe(device -> {
                                                    DataRepository.this.deviceName = device.getName();
                                                    deviceID = device.getUuid();
                                                    //    UserStore.getInstance(context).setDevice(device.getName(), device.getUuid());

                                                    liveData.postMutableLiveData(StatusResource.success(null));
                                                    networkStatusLiveData.postValue(StatusResource.success(network));

//                                                    getConfig(context);
                                                }, throwable -> {
                                                    Log.d("ERR", "addDevice10");
                                                    setErrorMessage(throwable, liveData.getMutableLiveData());
                                                    // setMutableLiveData(StatusResource.error(throwable.getMessage()));
                                                });
                                        compositeDisposable.add(disposableAddDevice);
                                    }
                                }
                            }
                        }, throwable -> {
                            setErrorMessage(throwable, liveData.getMutableLiveData());
                            Log.d("ERR", "getAllDevices2");
                            //   setMutableLiveData(StatusResource.error(NO_INTERNET_CONNECTION));
                        });
                compositeDisposable.add(disposableAllDevices);
            }
        }.getMutableLiveData();
    }


    public MutableLiveData<StatusResource<String>> getConfig(Context context) {
        return new NetworkBoundStatusResource<String>() {

            @Override protected void createCall() {
                NetworkBoundStatusResource<String> liveData = this;

                final HashMap<String, String> header = new HashMap<>();
                header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getToken());
                buildClientService(baseUrl);
                final Disposable configDisposable = clientApi.getConfig(deviceID, header)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(dataConfig -> {
                            final InputStream inputStream = dataConfig.byteStream();
                            final Scanner scanner = new Scanner(inputStream).useDelimiter(DELIMITER);
                            final String data = scanner.hasNext() ? scanner.next() : "";
                            postMutableLiveData(StatusResource.success(data));
                        }, throwable -> {
                            Log.d("ERR", "getConfig");
                            setErrorMessage(throwable, this.getMutableLiveData());
                        });
                compositeDisposable.add(configDisposable);
            }
        }.getMutableLiveData();
    }

    public MutableLiveData<StatusResource<Object>> createTunnel(final Context context, final String dataConfig) {
        return new NetworkBoundStatusResource<Object>() {

            @Override protected void createCall() {
                try {
                    final byte[] configBytes = dataConfig.getBytes();
                    final Config config = Config.parse(new ByteArrayInputStream(configBytes));
                    Application.getTunnelManager().create(TUNNEL_NAME, config).whenComplete((observableTunnel, throwable) -> {
                        if (observableTunnel != null) {
                            TunnelStore.getInstance(context).setTunnel(TUNNEL_NAME, dataConfig);
                            Application.getTunnelManager().setTunnelState(observableTunnel, State.DOWN);
                            UserStore.getInstance(context).setToken(UserStore.getInstance(context).getToken());
                            UserStore.getInstance(context).setDevice(deviceName, deviceID);
                            setHostName(context, baseUrl);
                            pendingTunnel = observableTunnel;
                            postMutableLiveData(StatusResource.success(null));
                        } else {
                            Log.d("ERR", "createTunnel11");
                            setErrorMessage(throwable, this.getMutableLiveData());
                        }
                    });
                } catch (Exception e) {
                    Log.d("ERR", "createTunnel12");
                    setErrorMessage(e, this.getMutableLiveData());
                }
            }
        }.getMutableLiveData();
    }

    private boolean isDeviceLoggedIn(Context context) {
        return !UserStore.DEVICE_DEFAULT_VALUE.equals(UserStore.getInstance(context).getDeviceName());
    }

    private String createErrorMessage(Call call, retrofit2.Response response) {
        return "Error: User agent: " + System.getProperty("http.agent") + ", Request body: " + call.request().body() + ", URL: " +
                call.request().url() + ", Code: " + response.code() + ", Message: " +
                response.message();
    }

    public boolean isUserLoggedIn(Context context) {
        return !UserStore.USER_TOKEN_DEFAULT_VALUE.equals(UserStore.getInstance(context).getToken());
    }

    private String getBrand() {
        final String brand = Build.MANUFACTURER;
        return capitalize(brand);
    }


    private String capitalize(final String brand) {
        if (brand == null || brand.isEmpty()) {
            return "";
        }
        final char first = brand.charAt(0);
        return Character.isUpperCase(first) ? brand : Character.toUpperCase(first) + brand.substring(1);
    }

    private String getDeviceID(final Context context) {
        return Settings.Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
    }

    private String getDeviceModel() {
        return Build.MODEL;
    }

    public void clearDisposable() {
        if (compositeDisposable != null) {
            compositeDisposable.clear();
        }
    }


    private Config parseConfig(final String data) throws IOException, BadConfigException {
        final byte[] configText = data.getBytes();
        return Config.parse(new ByteArrayInputStream(configText));
    }


    private ObservableTunnel createTunnel(final Context context, final boolean stateTunnel) {
        //TODO implement config is null case
        final Config config;
        try {
            config = parseConfig(TunnelStore.getInstance(context).getConfig());
        } catch (final IOException | BadConfigException e) {
            return null;
        }
        final String name = TunnelStore.getInstance(context).getTunnelName();
        return (stateTunnel) ? new ObservableTunnel(Application.getTunnelManager(), name, config, State.UP)
                : new ObservableTunnel(Application.getTunnelManager(), name, config, State.DOWN);
    }

    private void getTunnel(final Context context, final boolean connectionStateFlag) {

        final String tunnelName = TunnelStore.getInstance(context).getTunnelName();

        pendingTunnel = Application.getTunnelManager().getLastUsedTunnel();

        if (pendingTunnel == null) {
            pendingTunnel = Application.getTunnelManager().getTunnelByName(tunnelName);
        }

        if (pendingTunnel == null) {
            pendingTunnel = createTunnel(context, connectionStateFlag);
        }
    }

    public boolean isVPNConnected(final Context context, final boolean connectionStateFlag) {
        getTunnel(context, connectionStateFlag);
        return pendingTunnel.getState() == State.DOWN;
    }

    public MutableLiveData<Boolean> connect(final Boolean checked, final Context context) {
        final MutableLiveData<Boolean> liveData = new MutableLiveData<>();
        if (pendingTunnel != null) {
            Application.getBackendAsync().thenAccept(backend -> {
                if (backend instanceof GoBackend) {
                    final Intent intent = GoBackend.VpnService.prepare(context);
                    if (intent != null) {
                        if (context instanceof MainActivity) {
                            ((MainActivity) context).startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
                            return;
                        }
                    }
                }
                connectWithPermission(checked, context).observe((LifecycleOwner) context,
                        aBoolean -> liveData.postValue(aBoolean)
                );
            });
        }
        return liveData;
    }

    public MutableLiveData<Boolean> connectWithPermission(final boolean checked, final Context context) {
        final MutableLiveData<Boolean> liveData = new MutableLiveData<>();
        pendingTunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete((observableTunnelState, throwable) -> {
            if (throwable == null) {
                if (observableTunnelState == State.DOWN) {
                    liveData.postValue(false);
                } else {
                    liveData.postValue(true);
                }
            } else {
                Toast.makeText(context, context.getString(R.string.failed_bubble), Toast.LENGTH_SHORT).show();
            }
        });
        return liveData;
    }


    public void setHostName(Context context, String hostname) {
        UserStore.getInstance(context).setHostname(hostname);
    }

    public String getHostname(final Context context) {
        return UserStore.getInstance(context).getHostname();
    }

    private <T> void setErrorMessage(final Throwable throwable, final MutableLiveData<StatusResource<T>> liveData) {
        if (throwable instanceof IOException) {
            liveData.postValue(StatusResource.error(NO_INTERNET_CONNECTION));
            return;
        }
        if (throwable instanceof HttpException) {
            final Response<?> response = ((HttpException) throwable).response();
            if (response != null) {
                final int code = response.code();
                if (code == HTTP_500_ERR_CODE) {
                    final String requestURL = ((HttpException) throwable).response().raw().request().url().toString();
                    final String requestMethod = ((HttpException) throwable).response().raw().request().method();
                    final String requestBody = bodyToString(((HttpException) throwable).response().raw().request());
                    final String stackTrace = Arrays.toString(throwable.getStackTrace());
                    final String message = "URL:" + requestURL + '\n' +
                            "BODY:" + requestBody + '\n' +
                            "METHOD:" + requestMethod + '\n' +
                            "STACK_TRACE:" + stackTrace;
                    liveData.postValue(StatusResource.error(message));
                } else if (code == HTTP_422_ERR_CODE) {
                    try (final ResponseBody responseBody = response.errorBody()) {
                        if (responseBody != null) {
                            final UserErrorBody[] errorBody = ClientService.getInstance()
                                    .getErrorBodyAs(responseBody, UserErrorBody[].class);
                            if (errorBody.length > 0) {
                                if (errorBody[0].isAccountLocked()) {
                                    liveData.postValue(StatusResource.auth(MFAuthType.SAGE));
                                }
                            }
                        }
                    } catch (final IOException ex) {
                        Log.e("ERR", "Cannot deserialize error body while checking account locked error.", ex);
                        liveData.postValue(StatusResource.error(LOGIN_FAILED));
                    }
                } else {
                    Log.e("ERR", "Unhandled http error.", throwable);
                    liveData.postValue(StatusResource.error(LOGIN_FAILED));
                }
            }
        }
    }

    private String bodyToString(final Request request) {
        try {
            final Request copy = request.newBuilder().build();
            final Buffer buffer = new Buffer();
            final RequestBody body = copy.body();
            if (body != null) {
                body.writeTo(buffer);
            }
            return buffer.readUtf8();
        } catch (final IOException e) {
            return "did not work";
        }
    }

    public void removeSharedPreferences(final Context context) {
        context.getSharedPreferences(UserStore.USER_SHARED_PREF, 0).edit().clear().apply();
        context.getSharedPreferences(TunnelStore.TUNNEL_SHARED_PREF, 0).edit().clear().apply();
    }

    public void deleteTunnel() {
        if (pendingTunnel != null) {
            Application.getTunnelManager().delete(pendingTunnel);
            pendingTunnel = null;
        }
    }

    public MutableLiveData<StatusResource<Boolean>> isHaveTunnel(final Context context) {
        return  new NetworkBoundStatusResource<Boolean>(){
            @Override protected void createCall() {
                final byte[] configBytes = UserStore.getInstance(context).getConfig().getBytes();
                Config config = null;
                try {
                    config = Config.parse(new ByteArrayInputStream(configBytes));
                } catch (IOException ex) {
                    ex.printStackTrace();
                } catch (BadConfigException ex) {
                    ex.printStackTrace();
                }
                Application.getTunnelManager().create(TUNNEL_NAME, config).whenComplete((observableTunnel, throwable) -> {
                    if (observableTunnel != null) {
                        TunnelStore.getInstance(context).setTunnel(TUNNEL_NAME, UserStore.getInstance(context).getConfig());
                        Application.getTunnelManager().setTunnelState(observableTunnel, State.DOWN);
                        UserStore.getInstance(context).setToken(UserStore.getInstance(context).getToken());
                        UserStore.getInstance(context).setDevice(deviceName, deviceID);
                        setHostName(context, baseUrl);
                        postMutableLiveData(StatusResource.success(false));
                    } else {
                        postMutableLiveData(StatusResource.success(true));
                    }
                });
            }
        }.getMutableLiveData();
    }
}
