package com.example.vastlib.service

import android.net.Uri
import android.util.Log
import com.example.vastlib.entity.action.*
import com.example.vastlib.entity.http.Request
import com.example.vastlib.entity.http.Response
import com.example.vastlib.entity.report.ActionExec
import com.example.vastlib.entity.task.TaskConfig
import com.example.vastlib.service.HttpActionExecService.Companion.pattern
import com.example.vastlib.utils.HttpUtils
import com.example.vastlib.utils.HttpUtils.HttpRequest.call
import com.example.vastlib.utils.JSONUtils
import com.example.vastlib.utils.JSONUtils.mapListToJsonString
import com.example.vastlib.utils.JSONUtils.toJsonString
import com.example.vastlib.utils.LogUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URI
import java.net.URL
import java.nio.charset.StandardCharsets
import kotlin.time.DurationUnit
import kotlin.time.toDuration

class HttpActionExecService(
    private val action: HttpAction, override val taskConfig: TaskConfig
) : BaseActionExecService(taskConfig = taskConfig) {
    companion object {
        private const val MAX_REDIRECT_COUNT = 30
        val pattern = Regex("^[a-zA-Z0-9]+://.*")
    }

    suspend fun executeAction(onFinish: (List<ActionExec>) -> Unit = {}) {
        val actionExecList = mutableListOf<ActionExec>()
        var currentStep = taskConfig.current_step
        runCatching {
            action.delay.takeIf { it > 0 }?.apply {
                delay(this.toDuration(DurationUnit.SECONDS))
            }
            val actionRequest = action.request ?: throw NullPointerException("request is null")
            amendActionRequest(actionRequest)
            var httpRequest = buildHttpRequest(actionRequest)
            var httpResponse: Response? = null
            var proceedTask = false
            var stepCallHttpCount = 1
            if (action.async) {
                makeAsyncRequest(httpRequest)
                val actionExec = genActionExec(httpRequest, null, stepCallHttpCount)
                actionExec.respCode = ASYNC_EXEC_CODE
                actionExecList += actionExec
                proceedTask = true
            } else {
                httpResponse = httpRequest.call()
                val actionExec = genActionExec(httpRequest, httpResponse, stepCallHttpCount)
                actionExecList += actionExec
                when (httpResponse.code) {
                    in 100 until 200, in 200 until 300 -> {
                        proceedTask = true
                    }
                    in 300 until 400 -> {
                        var locationList: List<String>? =
                            httpResponse.headers[HttpUtils.Params.REQUEST_HEADER_LOCATION]

                        var redirectUrl: String = ""
                        if (!locationList.isNullOrEmpty()) {
                            redirectUrl = locationList.first()
                        }
                        while ((httpResponse != null && httpResponse.code >= 300 && httpResponse.code < 400 && stepCallHttpCount <= MAX_REDIRECT_COUNT && !redirectUrl.isNullOrBlank()) && redirectUrl.isHttpRedirect()) {
                            runCatching {
                                stepCallHttpCount++
                                httpRequest =
                                    buildRedirectHttpRequest(redirectUrl, httpRequest.url)
                                httpResponse = httpRequest.call()
                                if (httpResponse!!.code !in 300 until 400) {
                                    val responseHeaders = httpResponse?.headers
                                    val map = mutableMapOf<String, List<String>>()
                                    if (!responseHeaders.isNullOrEmpty()) {
                                        map += responseHeaders
                                    }
                                    if (!responseHeaders.isNullOrEmpty()) {
                                        val referer =
                                            httpRequest.headers.filter { it.key == HttpUtils.Params.REQUEST_HEADER_REFERER }
                                                .map { it.value }.toList()
                                        map[HttpUtils.Params.REQUEST_HEADER_REFERER] = referer
                                    }
                                    httpResponse?.headers = map
                                }

                                locationList =
                                    httpResponse?.headers?.get(HttpUtils.Params.REQUEST_HEADER_LOCATION)
                                redirectUrl = ""
                                if (!locationList.isNullOrEmpty()) {
                                    redirectUrl = locationList!!.first()
                                }
                                proceedTask = true
                                genActionExec(httpRequest, httpResponse, stepCallHttpCount)
                            }.onFailure { e ->
                                LogUtils.error(throwable = e)
                                proceedTask = action.skip_error
                                actionExecList += genActionExec(
                                    httpRequest, null, stepCallHttpCount
                                ).apply {
                                    respCode =
                                        HttpUtils.ErrorCode.ERROR_CODE_HTTP_BUILD_CONNECTION_FAILED
                                }
                            }.onSuccess {
                                actionExecList += it
                            }
                        }
                    }
                    else -> {
                        proceedTask = action.skip_error
                    }
                }
            }

            if (proceedTask) {
                httpResponse?.apply {
                    extractResponseVariableToCache(action, httpRequest, httpResponse)
                    val nextStep = httpGetNextStepIndex(
                        action.next, httpRequest, httpResponse, currentStep
                    )
                    taskConfig.current_step = nextStep
                } ?: let {
                    taskConfig.current_step = ++currentStep
                }
            } else {
                taskConfig.current_step = Int.MAX_VALUE
            }
        }.onFailure {
            LogUtils.error(throwable = it)
            val actionExec = genExceptionActionExec(
                action, ERROR_CODE_HTTP_ACTION_EXEC_FAILED, Log.getStackTraceString(it)
            )
            actionExecList += actionExec
            if (action.skip_error) {
                taskConfig.current_step = ++currentStep
            } else {
                taskConfig.current_step = Int.MAX_VALUE
            }
        }
        onFinish(actionExecList)
    }

    private fun httpGetNextStepIndex(
        next: List<Next>, httpRequest: Request, httpResponse: Response?, currentStep: Int
    ): Int {
        val wholeResponse = genWholeResponse(httpRequest, httpResponse)
        return getNextStepIndex(next, wholeResponse, currentStep)
    }

    private fun extractResponseVariableToCache(
        action: HttpAction, httpRequest: Request, httpResponse: Response?
    ) {
        action.response?.let { actionResponse ->
            httpResponse?.headers?.let { responseHeaders ->
                extractCookieVariableToCache(actionResponse.cookies, responseHeaders)
                extractHeaderVariableToCache(actionResponse.headers, responseHeaders)
                extractBodyVariableToCache(
                    action, genWholeResponse(httpRequest, httpResponse), httpResponse.data
                )
            }
        }
    }

    private fun genWholeResponse(httpRequest: Request, httpResponse: Response?): String {
        return """
            [${httpRequest.url}]${if (httpResponse?.data?.isNotEmpty() == true) String(httpResponse.data) else ""}[${
            httpResponse?.headers?.let {
                if (it.isEmpty()) ""
                else it.mapListToJsonString()
            }
        }] 
        """.trimIndent()
    }

    private fun extractHeaderVariableToCache(
        headers: List<NameVariable>, responseHeaders: Map<String, List<String>>
    ) {
        if (headers.isEmpty() || responseHeaders.isEmpty()) return
        headers.forEach { nameVariable ->
            responseHeaders[nameVariable.name]?.firstOrNull()?.apply {
                taskConfig.variable_cache[nameVariable.variable] = this
            }

        }
    }

    private fun extractCookieVariableToCache(
        cookies: List<NameVariable>, responseHeaders: Map<String, List<String>>
    ) {
        if (cookies.isEmpty()) return
        runCatching {
            val cookieList = responseHeaders[HttpUtils.Params.RESPONSE_HEADER_SET_COOKIE]
            if (!cookieList.isNullOrEmpty()) {
                cookies.forEach { nameVariable ->
                    cookieList.forEach { cookie ->
                        val cookieValues = cookie.split(";")
                        cookieValues.forEach { cookieValue ->
                            val keyPair = cookieValue.split("=", limit = 2)
                            val key = keyPair.first().trim()
                            val value = keyPair.getOrElse(1) { "" }.trim()
                            if (key == nameVariable.name) {
                                taskConfig.variable_cache[nameVariable.variable] = value
                            }
                        }
                    }
                }
            }

        }.onFailure {
            LogUtils.error(throwable = it)
        }
    }

    private fun makeAsyncRequest(httpRequest: Request) = scope.launch {
        var stepCallHttpCount = 0
        var asyncRequest: Request = httpRequest.copy()
        var asyncResponse = asyncRequest.call()
        var locationList: MutableList<String>
        var redirectUrl: String? = null
        if (asyncResponse.code in 300 until 400) {
            locationList =
                asyncResponse.headers[HttpUtils.Params.REQUEST_HEADER_LOCATION]?.toMutableList()
                    ?: mutableListOf()
            redirectUrl = locationList.firstOrNull()
        }
        while (asyncResponse.code in 300 until 400 && stepCallHttpCount <= MAX_REDIRECT_COUNT && !redirectUrl.isNullOrBlank() && redirectUrl.isHttpRedirect()) {
            kotlin.runCatching {
                stepCallHttpCount++
                asyncRequest = buildRedirectHttpRequest(redirectUrl!!, asyncRequest.url)
                asyncResponse = asyncRequest.call()
                locationList =
                    asyncResponse.headers[HttpUtils.Params.REQUEST_HEADER_LOCATION]?.toMutableList()
                        ?: mutableListOf()
                redirectUrl = locationList.firstOrNull()
            }.onFailure {
                LogUtils.error(throwable = it)
            }
        }
    }

    private fun genActionExec(
        httpRequest: Request, httpResponse: Response?, redirectCount: Int
    ): ActionExec {
        val actionExec = ActionExec(
            step = taskConfig.current_step,
            index = redirectCount,
            time = System.currentTimeMillis(),
            url = httpRequest.url,
            method = httpRequest.method,
            reqHeader = httpRequest.headers.toJsonString()
        )

        if (httpRequest.body.isNotEmpty()) {
            actionExec.reqData = String(httpRequest.body)
        }

        httpResponse?.let { response ->
            if (response.headers.isNotEmpty()) {
                kotlin.runCatching {
                    URL(httpRequest.url).apply {
                        URI(protocol, host, path, query, null).let { uri ->
                            taskConfig.cookie_manager.put(uri, response.headers)
                        }
                    }
                }.onFailure {
                    LogUtils.error(throwable = it)
                }

                actionExec.respCode = response.code
                if (response.data.isNotEmpty()) {
                    actionExec.respData = String(response.data)
                }
                actionExec.respHeader = response.headers.mapListToJsonString()
                actionExec.cost = httpResponse.endTime - httpResponse.startTime
            }

        }
        return actionExec
    }

    private fun buildHttpRequest(actionRequest: HttpActionRequest): Request = Request(
        url = actionRequest.url,
        method = actionRequest.method.uppercase(),
        body = actionRequest.data.toByteArray(),
        headers = nameValueToMap(actionRequest.headers)
    )


    private fun amendActionRequest(actionRequest: HttpActionRequest) {
        replaceVariableData(actionRequest.headers)
        replaceVariableData(actionRequest.cookies)
        replaceVariableData(actionRequest.params)

        amendBaseHeader(actionRequest.headers, true).apply {
            actionRequest.headers = this.toMutableList()
        }

        if (actionRequest.data.isNotBlank()) {
            actionRequest.data = getVariableData(actionRequest.data)
        }

        actionRequest.url = when (actionRequest.method.uppercase()) {
            HttpUtils.Params.REQUEST_METHOD_GET.uppercase() -> {
                buildGetUrl(actionRequest)
            }
            else -> {
                getVariableData(actionRequest.url)
            }
        }
        amendCookie(actionRequest)
        if (HttpUtils.Params.REQUEST_METHOD_GET.uppercase() != actionRequest.method.uppercase()) {
            val sendData: ByteArray = genPostData(actionRequest)
            if (sendData.isNotEmpty()) actionRequest.data = String(sendData)
        }

    }

    private fun genPostData(actionRequest: HttpActionRequest): ByteArray {
        var sendData: ByteArray = byteArrayOf()
        if (actionRequest.params.isNotEmpty()) {
            sendData = actionRequest.params.joinToString("&") {
                "${urlEncode(it.name)}=${urlEncode(it.value)}"
            }.toByteArray()
        }

        if (actionRequest.data.isNotBlank()) {
            sendData = actionRequest.data.toByteArray(Charsets.UTF_8)
        }
        return sendData
    }

    private fun amendCookie(actionRequest: HttpActionRequest) {
        val url = actionRequest.url
        val cookies: MutableSet<NameValue> = mutableSetOf()
        cookieFromCookieManager(url).apply {
            if (actionRequest.auto_cookie && isNotEmpty()) {
                cookies += this
            }
        }
        actionRequest.cookies.apply {
            if (isNotEmpty()) {
                forEach {
                    cookies.remove(it)
                    cookies += it
                }
            }
        }
        if (cookies.isNotEmpty()) {
            actionRequest.headers += buildCookie(cookies = cookies.toList())
        }
    }

    private fun buildGetUrl(actionRequest: HttpActionRequest): String {
        return getVariableData(actionRequest.url).let { u ->
            Uri.parse(u).buildUpon().apply {
                actionRequest.params.forEach { nameValue ->
                    this.appendQueryParameter(nameValue.name, nameValue.value)
                }
            }
        }.build().toString()
    }


    private fun String.isHttpRedirect(): Boolean = pattern.find(this)?.let {
        return@isHttpRedirect this.startsWith("http")
    } ?: true

    private fun buildRedirectHttpRequest(redirectUrl: String, originUrl: String): Request {
        val headers = genBaseHeaderMap(taskConfig).toMutableMap()
        headers[HttpUtils.Params.REQUEST_HEADER_REFERER] = originUrl
        val request = Request(
            method = HttpUtils.Params.REQUEST_METHOD_GET, headers = headers
        )

        URL(originUrl).apply {
            URL(this, redirectUrl.replace(" ", "%20")).apply {
                request.url = toString()
            }
        }

        cookieFromCookieManager(request.url).run {
            buildCookie(this.toList())
        }.run {
            headers.put(this.name, this.value)
        }

        return request
    }


}