/*
 *
 *   Nextcloud Talk application
 *
 *   @author Mario Danic
 *   Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.nextcloud.talk.controllers;

import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.*;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import autodagger.AutoInjector;
import butterknife.BindView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.events.CertificateEvent;
import com.nextcloud.talk.models.LoginData;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
import com.nextcloud.talk.utils.ssl.MagicTrustManager;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
import org.greenrobot.eventbus.EventBus;

import javax.inject.Inject;
import java.lang.reflect.Field;
import java.net.CookieManager;
import java.net.URLDecoder;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

@AutoInjector(NextcloudTalkApplication.class)
public class WebViewLoginController extends BaseController {

    public static final String TAG = "WebViewLoginController";

    private final String PROTOCOL_SUFFIX = "://";
    private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";

    @Inject
    UserUtils userUtils;
    @Inject
    AppPreferences appPreferences;
    @Inject
    ReactiveEntityStore<Persistable> dataStore;
    @Inject
    MagicTrustManager magicTrustManager;
    @Inject
    EventBus eventBus;
    @Inject
    CookieManager cookieManager;


    @BindView(R.id.webview)
    WebView webView;

    @BindView(R.id.progress_bar)
    ProgressBar progressBar;

    private String assembledPrefix;

    private Disposable userQueryDisposable;

    private String baseUrl;
    private boolean isPasswordUpdate;

    private String username;
    private String password;
    private int loginStep = 0;

    private boolean automatedLoginAttempted = false;

    public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) {
        this.baseUrl = baseUrl;
        this.isPasswordUpdate = isPasswordUpdate;
    }

    public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username, String password) {
        this.baseUrl = baseUrl;
        this.isPasswordUpdate = isPasswordUpdate;
        this.username = username;
        this.password = password;
    }

    public WebViewLoginController(Bundle args) {
        super(args);
    }

    private String getWebLoginUserAgent() {
        return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
                Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " ("
                + getResources().getString(R.string.nc_app_name) + ")";
    }

    @Override
    protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
        return inflater.inflate(R.layout.controller_web_view_login, container, false);
    }

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onViewBound(@NonNull View view) {
        super.onViewBound(view);
        NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);

        if (getActivity() != null) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }

        if (getActionBar() != null) {
            getActionBar().hide();
        }

        assembledPrefix = getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/";

        webView.getSettings().setAllowFileAccess(false);
        webView.getSettings().setAllowFileAccessFromFileURLs(false);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false);
        webView.getSettings().setDomStorageEnabled(true);
        webView.getSettings().setUserAgentString(getWebLoginUserAgent());
        webView.getSettings().setSaveFormData(false);
        webView.getSettings().setSavePassword(false);
        webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
        webView.clearCache(true);
        webView.clearFormData();
        webView.clearHistory();
        WebView.clearClientCertPreferences(null);

        CookieSyncManager.createInstance(getActivity());
        android.webkit.CookieManager.getInstance().removeAllCookies(null);

        Map<String, String> headers = new HashMap<>();
        headers.put("OCS-APIRequest", "true");

        webView.setWebViewClient(new WebViewClient() {
            private boolean basePageLoaded;

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (url.startsWith(assembledPrefix)) {
                    parseAndLoginFromWebView(url);
                    return true;
                }
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                loginStep++;

                if (!basePageLoaded) {
                    if (progressBar != null) {
                        progressBar.setVisibility(View.GONE);
                    }

                    if (webView != null) {
                        webView.setVisibility(View.VISIBLE);
                    }
                    basePageLoaded = true;
                }

                if (!TextUtils.isEmpty(username) && webView != null) {
                    if (loginStep == 1) {
                        webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };");
                    } else if (!automatedLoginAttempted) {
                        automatedLoginAttempted = true;
                        if (TextUtils.isEmpty(password)) {
                            webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '" + username + "';");
                        } else {
                            webView.loadUrl("javascript: {" +
                                    "document.getElementById('user').value = '" + username + "';" +
                                    "document.getElementById('password').value = '" + password + "';" +
                                    "document.getElementById('submit').click(); };");
                        }
                    }
                }

                super.onPageFinished(view, url);
            }

            @Override
            public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
                UserEntity userEntity = userUtils.getCurrentUser();

                String alias = null;
                if (!isPasswordUpdate) {
                    alias = appPreferences.getTemporaryClientCertAlias();
                }

                if (TextUtils.isEmpty(alias) && (userEntity != null)) {
                    alias = userEntity.getClientCertificate();
                }

                if (!TextUtils.isEmpty(alias)) {
                    String finalAlias = alias;
                    new Thread(() -> {
                        try {
                            PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias);
                            X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias);
                            if (privateKey != null && certificates != null) {
                                request.proceed(privateKey, certificates);
                            } else {
                                request.cancel();
                            }
                        } catch (KeyChainException | InterruptedException e) {
                            request.cancel();
                        }
                    }).start();
                } else {
                    KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> {
                        if (chosenAlias != null) {
                            appPreferences.setTemporaryClientCertAlias(chosenAlias);
                            new Thread(() -> {
                                PrivateKey privateKey = null;
                                try {
                                    privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias);
                                    X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias);
                                    if (privateKey != null && certificates != null) {
                                        request.proceed(privateKey, certificates);
                                    } else {
                                        request.cancel();
                                    }
                                } catch (KeyChainException | InterruptedException e) {
                                    request.cancel();
                                }
                            }).start();
                        } else {
                            request.cancel();
                        }
                    }, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null);
                }
            }

            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                try {
                    SslCertificate sslCertificate = error.getCertificate();
                    Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
                    f.setAccessible(true);
                    X509Certificate cert = (X509Certificate) f.get(sslCertificate);

                    if (cert == null) {
                        handler.cancel();
                    } else {
                        try {
                            magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic");
                            handler.proceed();
                        } catch (CertificateException exception) {
                            eventBus.post(new CertificateEvent(cert, magicTrustManager, handler));
                        }
                    }
                } catch (Exception exception) {
                    handler.cancel();
                }
            }

            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                super.onReceivedError(view, errorCode, description, failingUrl);
            }
        });

        webView.loadUrl(baseUrl + "/index.php/login/flow", headers);
    }

    private void dispose() {
        if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) {
            userQueryDisposable.dispose();
        }

        userQueryDisposable = null;
    }

    private void parseAndLoginFromWebView(String dataString) {
        LoginData loginData = parseLoginData(assembledPrefix, dataString);

        if (loginData != null) {
            dispose();

            UserEntity currentUser = userUtils.getCurrentUser();

            ApplicationWideMessageHolder.MessageType messageType = null;

            if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) {
                messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED;
            }

            if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) {
                ApplicationWideMessageHolder.getInstance().setMessageType(
                        ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION);

                if (!isPasswordUpdate) {
                    getRouter().popToRoot();
                } else {
                    getRouter().popCurrentController();
                }
            }

            ApplicationWideMessageHolder.MessageType finalMessageType = messageType;
            cookieManager.getCookieStore().removeAll();

            if (!isPasswordUpdate && finalMessageType == null) {
                Bundle bundle = new Bundle();
                bundle.putString(BundleKeys.KEY_USERNAME, loginData.getUsername());
                bundle.putString(BundleKeys.KEY_TOKEN, loginData.getToken());
                bundle.putString(BundleKeys.KEY_BASE_URL, loginData.getServerUrl());
                String protocol = "";

                if (baseUrl.startsWith("http://")) {
                    protocol = "http://";
                } else if (baseUrl.startsWith("https://")) {
                    protocol = "https://";
                }

                if (!TextUtils.isEmpty(protocol)) {
                    bundle.putString(BundleKeys.KEY_ORIGINAL_PROTOCOL, protocol);
                }

                getRouter().pushController(RouterTransaction.with(new AccountVerificationController
                        (bundle)).pushChangeHandler(new HorizontalChangeHandler())
                        .popChangeHandler(new HorizontalChangeHandler()));
            } else {
                if (isPasswordUpdate) {
                    if (currentUser != null) {
                        userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(),
                                null, null, null, true,
                                null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
                                .subscribeOn(Schedulers.newThread())
                                .observeOn(AndroidSchedulers.mainThread())
                                .subscribe(userEntity -> {
                                            if (finalMessageType != null) {
                                                ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
                                            }
                                            getRouter().popCurrentController();
                                        }, throwable -> dispose(),
                                        this::dispose);
                    }
                } else {
                    if (finalMessageType != null) {
                        ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
                    }
                    getRouter().popToRoot();

                }
            }
        }
    }

    private LoginData parseLoginData(String prefix, String dataString) {
        if (dataString.length() < prefix.length()) {
            return null;
        }

        LoginData loginData = new LoginData();

        // format is xxx://login/server:xxx&user:xxx&password:xxx
        String data = dataString.substring(prefix.length());

        String[] values = data.split("&");

        if (values.length != 3) {
            return null;
        }

        for (String value : values) {
            if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.setUsername(URLDecoder.decode(
                        value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
            } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.setToken(URLDecoder.decode(
                        value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
            } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.setServerUrl(URLDecoder.decode(
                        value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
            } else {
                return null;
            }
        }

        if (!TextUtils.isEmpty(loginData.getServerUrl()) && !TextUtils.isEmpty(loginData.getUsername()) &&
                !TextUtils.isEmpty(loginData.getToken())) {
            return loginData;
        } else {
            return null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        dispose();
    }

    @Override
    protected void onDestroyView(@NonNull View view) {
        super.onDestroyView(view);
        if (getActivity() != null) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
        }
    }
}