package com.adealink.weparty.webview.jsbridge

import android.os.Build
import android.text.TextUtils
import android.webkit.JavascriptInterface
import androidx.annotation.UiThread
import com.adealink.frame.data.json.froJsonErrorNull
import com.adealink.frame.data.json.toJsonErrorNull
import com.adealink.frame.locale.language.languageManager
import com.adealink.frame.log.Log
import com.adealink.frame.network.http.interceptor.ACCEPT_LANGUAGE
import com.adealink.frame.network.http.interceptor.KEY_API
import com.adealink.frame.network.http.interceptor.KEY_CHANNEL
import com.adealink.frame.network.http.interceptor.KEY_CONTENT_TYPE
import com.adealink.frame.network.http.interceptor.KEY_DEVICE
import com.adealink.frame.network.http.interceptor.KEY_NETWORK
import com.adealink.frame.network.http.interceptor.KEY_PACKAGE_NAME
import com.adealink.frame.network.http.interceptor.KEY_PLATFORM
import com.adealink.frame.network.http.interceptor.KEY_REQ_ID
import com.adealink.frame.network.http.interceptor.KEY_TIME
import com.adealink.frame.network.http.interceptor.KEY_TOKEN
import com.adealink.frame.network.http.interceptor.KEY_UDID
import com.adealink.frame.network.http.interceptor.KEY_VERSION_CODE
import com.adealink.frame.util.PackageUtil
import com.adealink.frame.util.getDeviceName
import com.adealink.frame.util.md5
import com.adealink.frame.util.runOnUiThread
import com.adealink.weparty.App
import com.adealink.weparty.BuildConfig
import com.adealink.weparty.storage.AppPref
import com.adealink.weparty.webview.IWebView
import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_JS_BRIDGE
import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
import com.adealink.weparty.webview.jsbridge.data.JSRequest
import com.adealink.weparty.webview.jsbridge.data.JSResponse
import com.adealink.weparty.webview.jsbridge.data.NativeMessage
import com.adealink.weparty.webview.jsbridge.manager.JSBridgeManager
import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
import kotlin.random.Random


class JSBridgeImpl(val webView: IWebView, private val interfaceName: String) : JSBridge {

    private val methodMap = hashMapOf<String, JSNativeMethod<Any, Any>>() //key: method name

    init {
        addNativeMethod(BridgeSupportJSNativeMethod())
    }

    override fun interfaceName(): String {
        return interfaceName
    }

    @Suppress("UNCHECKED_CAST")
    @UiThread
    override fun <T, R> addNativeMethod(method: JSNativeMethod<T, R>) {
        methodMap[method.methodName] = method as JSNativeMethod<Any, Any>
    }

    @UiThread
    override fun removeNativeMethod(methodName: String) {
        methodMap.remove(methodName)
    }

    override fun <T> notifyNativeMessage(message: NativeMessage<T>) {
        val messageStr = toJsonErrorNull(message)
        if (messageStr.isNullOrEmpty()) {
            Log.e(
                TAG_WEB_VIEW_JS_BRIDGE,
                "notifyNativeMessage, message is empty"
            )
            return
        }
        val script = "javascript:window.notifyMessage('$messageStr')"
        runOnUiThread {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                webView.evaluateJavascript(script, null)
            } else {
                webView.loadUrl(script)
            }
        }
    }

    @JavascriptInterface
    fun postMessageToNative(json: String?) {
        runOnUiThread { handleJSMessage(json) }
    }

    @JavascriptInterface
    fun requestHeader(): String {
        val currentTs = System.currentTimeMillis()
        val deviceName = getDeviceName()
        val headers = mutableMapOf<String, String>()
        headers[KEY_TOKEN] = AppPref.token
        headers[KEY_REQ_ID] = "$currentTs/${Random(100).nextInt()}/${deviceName}".md5()
        headers[KEY_UDID] = App.instance.deviceIdService.getMemOldDeviceId()
        headers[KEY_PACKAGE_NAME] = PackageUtil.getPackageName()
        headers[KEY_DEVICE] = deviceName
        headers[KEY_PLATFORM] = "1"
        headers[KEY_CHANNEL] = "official"
        headers[KEY_API] = "1"
        headers[KEY_VERSION_CODE] = BuildConfig.VERSION_CODE.toString()
        headers[KEY_NETWORK] = "5g"
        headers[KEY_TIME] = currentTs.toString()
        headers[KEY_CONTENT_TYPE] = "application/json"
        headers[ACCEPT_LANGUAGE] = languageManager?.getLanguageCode() ?: ""
        return (toJsonErrorNull(headers) ?: "").also {
            Log.d(TAG_WEB_VIEW_JS_BRIDGE, "requestHeader, header:$it")
        }
    }

    @UiThread
    private fun handleJSMessage(json: String?) {
        Log.d(TAG_WEB_VIEW_JS_BRIDGE, "handleJSMessage, json:${json}")
        val jsRequest = JSRequest.parseJson(json)
        val callbackBridge = CallbackBridge(jsRequest.callbackId, jsRequest.methodName)
        if (isDenied()) {
            callbackBridge.reject(JSResponse.JSError.CLIENT_DENIED)
            return
        }

        when (jsRequest.type) {
            JSRequest.TYPE_INVOKE_METHOD -> doInvokeMethod(jsRequest, callbackBridge)
            else -> callbackBridge.reject(JSResponse.JSError.UNKNOWN_REQUEST_TYPE)
        }
    }

    /**
     * 是否拒绝执行
     */
    private fun isDenied(): Boolean {
        val url = webView.getUrl() ?: ""
        val originalUrl = webView.getOriginalUrl() ?: ""
        Log.d(TAG_WEB_VIEW_JS_BRIDGE, "isDenied, url:${url}, originalUrl:${originalUrl}")
        return (!JSBridgeManager.instance.isWhite(url)
                && !JSBridgeManager.instance.isWhite(originalUrl)
                || JSBridgeManager.instance.isBlack(url)
                || JSBridgeManager.instance.isBlack(originalUrl))
    }

    private fun doInvokeMethod(jsRequest: JSRequest, callbackBridge: CallbackBridge) {
        val methodName = jsRequest.methodName
        if (methodName.isEmpty()) {
            callbackBridge.reject(JSResponse.JSError.REQUEST_METHOD_NAME_NULL)
            return
        }

        val nativeMethod = methodMap[methodName]
        if (nativeMethod == null) {
            callbackBridge.reject(JSResponse.JSError.CLIENT_NO_REGISTER_HANDLE_METHOD)
            return
        }

        val clazz = JSNativeMethod.classOfT(nativeMethod)
        if (clazz == null) {
            callbackBridge.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_TYPE_ERROR)
            return
        }

        val dataStr = jsRequest.data
        if (clazz == Any::class.java) {
            nativeMethod.handleMethodCall(Any(), callbackBridge)
            return
        }

        val data =
            if (clazz == Nothing::class.java || clazz == Void::class.java || dataStr.isNullOrEmpty()) {
                null
            } else if (clazz == String::class.java) {
                dataStr
            } else {
                froJsonErrorNull(dataStr, clazz)
            }

        if (data == null) {
            callbackBridge.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_PARSE_ERROR)
            return
        }

        nativeMethod.handleMethodCall(data, callbackBridge)
    }

    inner class CallbackBridge(override val callbackId: String, val methodName: String?) :
        JSBridgeCallback<Any> {

        private fun sendResponseToJS(response: JSResponse<Any>) {
            if (TextUtils.isEmpty(callbackId)) {
                Log.e(
                    TAG_WEB_VIEW_JS_BRIDGE,
                    "sendResponseToJS, methodName:${methodName}, response:${response}, callbackId is empty"
                )
                return
            }

            val responseStr = toJsonErrorNull(response)
            if (responseStr.isNullOrEmpty()) {
                Log.e(
                    TAG_WEB_VIEW_JS_BRIDGE,
                    "sendResponseToJS, methodName:${methodName}, response:${response}, to json error"
                )
                return
            }

            runOnUiThread { callJavascript("javascript:window.${callbackId}($responseStr)") }
        }

        private fun callJavascript(script: String) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                webView.evaluateJavascript(script, null)
            } else {
                webView.loadUrl(script)
            }
        }

        override fun resolve(data: Any) {
            Log.d(
                TAG_WEB_VIEW_JS_BRIDGE,
                "resolve, methodName:${methodName}, callbackId:${callbackId}, data:${data}"
            )
            sendResponseToJS(JSResponse(callbackId = this@CallbackBridge.callbackId, data = data))
        }

        override fun reject(error: JSResponse.JSError) {
            Log.e(
                TAG_WEB_VIEW_JS_BRIDGE,
                "reject, methodName:${methodName}, callbackId:${callbackId}, error:${error.message}"
            )
            sendResponseToJS(JSResponse(error.code, error.message, this@CallbackBridge.callbackId))
        }

        override fun reject(errorCode: Int, message: String?) {
            Log.e(
                TAG_WEB_VIEW_JS_BRIDGE,
                "reject, methodName:${methodName}, callbackId:${callbackId}, error:${message ?: ""}"
            )
            sendResponseToJS(JSResponse(errorCode, message ?: "", this@CallbackBridge.callbackId))
        }

    }

    inner class BridgeSupportJSNativeMethod :
        JSNativeMethod<Map<String, List<String>>, Map<String, Boolean>> {
        override val methodName: String = "isBridgeSupport"

        override fun handleMethodCall(
            data: Map<String, List<String>>,
            callback: JSBridgeCallback<Map<String, Boolean>>?
        ) {
            val jsNativeMethods = data["data"]

            if (jsNativeMethods == null) {
                callback?.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_TYPE_ERROR)
                return
            }
            val result = jsNativeMethods.associateWith { key -> methodMap.containsKey(key) }
            callback?.resolve(result)
        }
    }

}