/*
 *   Nextcloud Talk application
 *
 *   @author Mario Danic
 *   @author Andy Scherzinger
 *   Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
 *   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.graphics.Bitmap
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.util.Log
import android.view.View
import android.webkit.ClientCertRequest
import android.webkit.CookieSyncManager
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import autodagger.AutoInjector
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.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.controllers.base.NewBaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.nextcloud.talk.utils.ssl.MagicTrustManager
import de.cotech.hw.fido.WebViewFidoBridge
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 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.Locale
import javax.inject.Inject

@AutoInjector(NextcloudTalkApplication::class)
class WebViewLoginController(args: Bundle? = null) : NewBaseController(
    R.layout.controller_web_view_login,
    args
) {
    private val binding: ControllerWebViewLoginBinding by viewBinding(ControllerWebViewLoginBinding::bind)

    @Inject
    lateinit var userUtils: UserUtils

    @Inject
    lateinit var dataStore: ReactiveEntityStore<Persistable>

    @Inject
    lateinit var magicTrustManager: MagicTrustManager

    @Inject
    lateinit var eventBus: EventBus

    @Inject
    lateinit var cookieManager: CookieManager

    private var assembledPrefix: String? = null
    private var userQueryDisposable: Disposable? = null
    private var baseUrl: String? = null
    private var isPasswordUpdate = false
    private var username: String? = null
    private var password: String? = null
    private var loginStep = 0
    private var automatedLoginAttempted = false
    private var webViewFidoBridge: WebViewFidoBridge? = null

    constructor(baseUrl: String?, isPasswordUpdate: Boolean) : this() {
        this.baseUrl = baseUrl
        this.isPasswordUpdate = isPasswordUpdate
    }

    constructor(baseUrl: String?, isPasswordUpdate: Boolean, username: String?, password: String?) : this() {
        this.baseUrl = baseUrl
        this.isPasswordUpdate = isPasswordUpdate
        this.username = username
        this.password = password
    }

    private val webLoginUserAgent: String
        get() = (
            Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
                Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) +
                " " +
                Build.MODEL +
                " (" +
                resources!!.getString(R.string.nc_app_product_name) +
                ")"
            )

    @SuppressLint("SetJavaScriptEnabled")
    override fun onViewBound(view: View) {
        super.onViewBound(view)
        activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT

        actionBar?.hide()

        assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
        binding.webview.settings.allowFileAccess = false
        binding.webview.settings.allowFileAccessFromFileURLs = false
        binding.webview.settings.javaScriptEnabled = true
        binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false
        binding.webview.settings.domStorageEnabled = true
        binding.webview.settings.setUserAgentString(webLoginUserAgent)
        binding.webview.settings.saveFormData = false
        binding.webview.settings.savePassword = false
        binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
        binding.webview.clearCache(true)
        binding.webview.clearFormData()
        binding.webview.clearHistory()
        WebView.clearClientCertPreferences(null)
        webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, binding.webview)
        CookieSyncManager.createInstance(activity)
        android.webkit.CookieManager.getInstance().removeAllCookies(null)
        val headers: MutableMap<String, String> = HashMap()
        headers.put("OCS-APIRequest", "true")
        binding.webview.webViewClient = object : WebViewClient() {
            private var basePageLoaded = false
            override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
                webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
                return super.shouldInterceptRequest(view, request)
            }

            override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                webViewFidoBridge?.delegateOnPageStarted(view, url, favicon)
            }

            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                if (url.startsWith(assembledPrefix!!)) {
                    parseAndLoginFromWebView(url)
                    return true
                }
                return false
            }

            @Suppress("Detekt.TooGenericExceptionCaught")
            override fun onPageFinished(view: WebView, url: String) {
                try {
                    loginStep++
                    if (!basePageLoaded) {
                        binding.progressBar.visibility = View.GONE
                        binding.webview.visibility = View.VISIBLE

                        basePageLoaded = true
                    }
                    if (!TextUtils.isEmpty(username)) {
                        if (loginStep == 1) {
                            binding.webview.loadUrl(
                                "javascript: {document.getElementsByClassName('login')[0].click(); };"
                            )
                        } else if (!automatedLoginAttempted) {
                            automatedLoginAttempted = true
                            if (TextUtils.isEmpty(password)) {
                                binding.webview.loadUrl(
                                    "javascript:var justStore = document.getElementById('user').value = '$username';"
                                )
                            } else {
                                binding.webview.loadUrl(
                                    "javascript: {" +
                                        "document.getElementById('user').value = '" + username + "';" +
                                        "document.getElementById('password').value = '" + password + "';" +
                                        "document.getElementById('submit').click(); };"
                                )
                            }
                        }
                    }
                } catch (npe: NullPointerException) {
                    // view binding can be null
                    // since this is called asynchronously and UI might have been destroyed in the meantime
                    Log.i(TAG, "UI destroyed - view binding already gone")
                }

                super.onPageFinished(view, url)
            }

            override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) {
                val userEntity = userUtils.currentUser
                var alias: String? = null
                if (!isPasswordUpdate) {
                    alias = appPreferences!!.temporaryClientCertAlias
                }
                if (TextUtils.isEmpty(alias) && userEntity != null) {
                    alias = userEntity.clientCertificate
                }
                if (!TextUtils.isEmpty(alias)) {
                    val finalAlias = alias
                    Thread {
                        try {
                            val privateKey = KeyChain.getPrivateKey(activity!!, finalAlias!!)
                            val certificates = KeyChain.getCertificateChain(
                                activity!!, finalAlias
                            )
                            if (privateKey != null && certificates != null) {
                                request.proceed(privateKey, certificates)
                            } else {
                                request.cancel()
                            }
                        } catch (e: KeyChainException) {
                            request.cancel()
                        } catch (e: InterruptedException) {
                            request.cancel()
                        }
                    }.start()
                } else {
                    KeyChain.choosePrivateKeyAlias(
                        activity!!,
                        { chosenAlias: String? ->
                            if (chosenAlias != null) {
                                appPreferences!!.temporaryClientCertAlias = chosenAlias
                                Thread {
                                    var privateKey: PrivateKey? = null
                                    try {
                                        privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias)
                                        val certificates = KeyChain.getCertificateChain(
                                            activity!!, chosenAlias
                                        )
                                        if (privateKey != null && certificates != null) {
                                            request.proceed(privateKey, certificates)
                                        } else {
                                            request.cancel()
                                        }
                                    } catch (e: KeyChainException) {
                                        request.cancel()
                                    } catch (e: InterruptedException) {
                                        request.cancel()
                                    }
                                }.start()
                            } else {
                                request.cancel()
                            }
                        },
                        arrayOf("RSA", "EC"),
                        null,
                        request.host,
                        request.port,
                        null
                    )
                }
            }

            @Suppress("Detekt.TooGenericExceptionCaught")
            override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
                try {
                    val sslCertificate = error.certificate
                    val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate")
                    f.isAccessible = true
                    val cert = f[sslCertificate] as X509Certificate
                    if (cert == null) {
                        handler.cancel()
                    } else {
                        try {
                            magicTrustManager.checkServerTrusted(arrayOf(cert), "generic")
                            handler.proceed()
                        } catch (exception: CertificateException) {
                            eventBus.post(CertificateEvent(cert, magicTrustManager, handler))
                        }
                    }
                } catch (exception: Exception) {
                    handler.cancel()
                }
            }

            override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
                super.onReceivedError(view, errorCode, description, failingUrl)
            }
        }
        binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
    }

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

    private fun parseAndLoginFromWebView(dataString: String) {
        val loginData = parseLoginData(assembledPrefix, dataString)
        if (loginData != null) {
            dispose()
            val currentUser = userUtils.currentUser
            var messageType: ApplicationWideMessageHolder.MessageType? = null
            if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.username, baseUrl)) {
                messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED
            }
            if (userUtils.checkIfUserIsScheduledForDeletion(loginData.username, baseUrl)) {
                ApplicationWideMessageHolder.getInstance().setMessageType(
                    ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
                )
                if (!isPasswordUpdate) {
                    router.popToRoot()
                } else {
                    router.popCurrentController()
                }
            }
            val finalMessageType = messageType
            cookieManager.cookieStore.removeAll()
            if (!isPasswordUpdate && finalMessageType == null) {
                val bundle = Bundle()
                bundle.putString(KEY_USERNAME, loginData.username)
                bundle.putString(KEY_TOKEN, loginData.token)
                bundle.putString(KEY_BASE_URL, loginData.serverUrl)
                var protocol = ""
                if (baseUrl!!.startsWith("http://")) {
                    protocol = "http://"
                } else if (baseUrl!!.startsWith("https://")) {
                    protocol = "https://"
                }
                if (!TextUtils.isEmpty(protocol)) {
                    bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
                }
                router.pushController(
                    RouterTransaction.with(AccountVerificationController(bundle))
                        .pushChangeHandler(HorizontalChangeHandler())
                        .popChangeHandler(HorizontalChangeHandler())
                )
            } else {
                if (isPasswordUpdate) {
                    if (currentUser != null) {
                        userQueryDisposable = userUtils.createOrUpdateUser(
                            null,
                            loginData.token,
                            null,
                            null,
                            "",
                            java.lang.Boolean.TRUE,
                            null,
                            currentUser.id,
                            null,
                            appPreferences!!.temporaryClientCertAlias,
                            null
                        )
                            .subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread())
                            .subscribe(
                                {
                                    if (finalMessageType != null) {
                                        ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
                                    }
                                    val data = Data.Builder().putString(
                                        PushRegistrationWorker.ORIGIN,
                                        "WebViewLoginController#parseAndLoginFromWebView"
                                    ).build()
                                    val pushRegistrationWork = OneTimeWorkRequest.Builder(
                                        PushRegistrationWorker::class.java
                                    )
                                        .setInputData(data)
                                        .build()
                                    WorkManager.getInstance().enqueue(pushRegistrationWork)
                                    router.popCurrentController()
                                },
                                { dispose() }
                            ) { dispose() }
                    }
                } else {
                    if (finalMessageType != null) {
                        // FIXME when the user registers a new account that was setup before (aka
                        //  ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
                        //  The token is not updated in the database and therefor the account not visible/usable
                        ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
                    }
                    router.popToRoot()
                }
            }
        }
    }

    private fun parseLoginData(prefix: String?, dataString: String): LoginData? {
        if (dataString.length < prefix!!.length) {
            return null
        }
        val loginData = LoginData()

        // format is xxx://login/server:xxx&user:xxx&password:xxx
        val data: String = dataString.substring(prefix.length)
        val values: Array<String> = data.split("&").toTypedArray()
        if (values.size != PARAMETER_COUNT) {
            return null
        }
        for (value in values) {
            if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.username = URLDecoder.decode(
                    value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
                )
            } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.token = URLDecoder.decode(
                    value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
                )
            } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
                loginData.serverUrl = URLDecoder.decode(
                    value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
                )
            } else {
                return null
            }
        }
        return if (!TextUtils.isEmpty(loginData.serverUrl) && !TextUtils.isEmpty(loginData.username) &&
            !TextUtils.isEmpty(loginData.token)
        ) {
            loginData
        } else {
            null
        }
    }

    override fun onAttach(view: View) {
        super.onAttach(view)
        if (activity != null && resources != null) {
            DisplayUtils.applyColorToStatusBar(
                activity,
                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
            )
            DisplayUtils.applyColorToNavigationBar(
                activity!!.window,
                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
            )
        }
    }

    public override fun onDestroy() {
        super.onDestroy()
        dispose()
    }

    override fun onDestroyView(view: View) {
        super.onDestroyView(view)
        activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
    }

    init {
        sharedApplication!!.componentApplication.inject(this)
    }

    override val appBarLayoutType: AppBarLayoutType
        get() = AppBarLayoutType.EMPTY

    companion object {
        const val TAG = "WebViewLoginController"
        private const val PROTOCOL_SUFFIX = "://"
        private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
        private const val PARAMETER_COUNT = 3
    }
}
