package co.id.datacomsolusindo.ipphonebridge

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectWriter
import org.apache.commons.lang3.StringUtils
import org.apache.logging.log4j.LogManager
import org.ini4j.Wini
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.core.io.ResourceLoader
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Component
import java.io.File
import java.io.IOException
import java.io.Serializable
import java.net.InetAddress
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.TimeUnit
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import kotlin.streams.toList


@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class SimpleCORSFilter : Filter {
    @Throws(ServletException::class)
    override fun init(fc: FilterConfig?) {
    }

    @Throws(IOException::class, ServletException::class)
    override fun doFilter(
        req: ServletRequest, resp: ServletResponse,
        chain: FilterChain
    ) {
        val response = resp as HttpServletResponse
        val request = req as HttpServletRequest
        response.setHeader("Access-Control-Allow-Origin", "*")
        response.setHeader("Access-Control-Allow-Methods", "PATCH,PUT,POST,GET,OPTIONS,DELETE")
        response.setHeader("Access-Control-Max-Age", "3600")
        response.setHeader(
            "Access-Control-Allow-Headers",
            "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN"
        )
        if ("OPTIONS".equals(request.method, ignoreCase = true)) {
            response.status = HttpServletResponse.SC_OK
        } else {
            chain.doFilter(req, resp)
        }
    }

    override fun destroy() {}
}

@Component
@Order(1)
class BridgeFilter(private val template: SimpMessagingTemplate, val resourceLoader: ResourceLoader) : Filter {
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        val startTime = System.nanoTime()
        val req = request as HttpServletRequest
        val res = response as HttpServletResponse
        val splitPath = req.requestURI.split("/")
        val client = splitPath[1]
        val toPath = "/" + splitPath.takeLast(splitPath.size - 1).joinToString("/")
        val clientNumber = client.toIntOrNull()
        if (clientNumber != null && clientNumber > 0 && clientNumber <= 999) {
            val search = isResourceAvailable(toPath)
            if (search != null && !search.endsWith("assets/bootstrap.js")) {
                res.sendRedirect("/resources/$search")
            } else {
                LogManager.getLogger("client-$client")
                    .info("Request Start, UA: " + req.getHeader("User-Agent") + " From: " + InetAddress.getLocalHost().hostAddress + " To: " + req.requestURL.toString() + "?" + req.queryString)

                val reqId = UUID.randomUUID().toString()
                try {
                    val headerMap = req.headerNames.toList().associateBy({ it }, { req.getHeader(it) })
                    val rb = RequestBuilder(reqId, toPath, HttpMethod.valueOf(req.method), headerMap.toMutableMap())
                    rb.queryString = req.queryString
                    AppLog.write(this.javaClass).info("content type = " + req.contentType)
                    if (req.contentType?.toLowerCase()?.contains("multipart/form-data") == true) {
                        rb.parts =
                            req.parts.map {
                                val subFilename = if (it.submittedFileName == null) {
                                    UUID.randomUUID().toString().take(5)
                                } else {
                                    it.submittedFileName
                                }

                                val fb64 = Base64.getEncoder().encodeToString(it.inputStream.readBytes())
                                FilePart(it.name, subFilename, fb64)
                            }.toMutableList()
                    } else {
                        val body = req.reader.lines().toList().joinToString(System.lineSeparator())
                        rb.body = body
                    }
                    Singletons.requestInstance.getOrPut(reqId) { RequestQue(reqId, rb, null) }
                    val ow: ObjectWriter = ObjectMapper().writer().withDefaultPrettyPrinter()
                    val chunkSize = 2000
                    val partStr = ow.writeValueAsString(rb).chunked(chunkSize)
                    val partialID = UUID.randomUUID().toString()
                    val st2 = System.nanoTime()
                    if (!ClientHolder.get().containsKey(StringUtils.leftPad(clientNumber.toString(), 3, '0'))) {
//                        subscribeGW /topic/request/reqID
//                        sendGW "/app/partial/$client"
                        chain.doFilter(request, response)
                        return
                    } else {
                        partStr.forEachIndexed { idx, it ->
                            template.convertAndSend(
                                "/topic/partial/$client",
                                PartialData(partialID, partStr.size, idx + 1, it, st2)
                            )
                        }


                    }

                    var i = 0

                    while (Singletons.responseQue.getOrElse(reqId) { null } == null) {
                        TimeUnit.MILLISECONDS.sleep(50)
                        i++
                        if (i >= 600 * partStr.size) {
                            res.status = HttpStatus.REQUEST_TIMEOUT.value()
                            res.writer.write("{\"message\":\"Request timeout. Client not responding\"}")
                            res.flushBuffer()
                            Singletons.requestInstance.remove(reqId)
                            Singletons.buildSocketChunkData.remove(reqId)
                            Singletons.responseQue.remove(reqId)
                            val endTime = System.nanoTime()
                            val duration =
                                (endTime - startTime).toDouble() / 1000000 //divide by 1000000 to get milliseconds.
                            LogManager.getLogger("client-$client")
                                .info("Request failed, No Response. Time: $duration  UA: " + req.getHeader("User-Agent") + " From: " + InetAddress.getLocalHost().hostAddress + " To: " + req.requestURL.toString() + "?" + req.queryString)
//                        unsubscribeGW /topic/request/reqID
                            ClientHolder.addFailedRequest(client)
                            return
                        }
                    }

                    val resFromSocket = Singletons.responseQue.getOrElse(reqId) { null }

                    res.status = resFromSocket?.statusCode ?: 500
                    resFromSocket?.headers?.let {
                        it.entries
                            .filter { f -> resFromSocket.statusCode < 400 || (resFromSocket.statusCode >= 400 && f.key == "Content-Type") }
                            .forEach { en ->
                                res.setHeader(en.key, en.value.joinToString(","))
                            }
                    }

                    resFromSocket?.body?.let {
                        res.outputStream.write(it)
                    }

                    res.flushBuffer()

                    Singletons.requestInstance.remove(reqId)
                    Singletons.buildSocketChunkData.remove(reqId)
                    Singletons.responseQue.remove(reqId)
                    //                        unsubscribeGW /topic/request/reqID
                    val endTime = System.nanoTime()

                    val duration =
                        (endTime - startTime).toDouble() / 1000000 //divide by 1000000 to get milliseconds.
                    LogManager.getLogger("client-$client")
                        .info("Request Success. Time: $duration  UA: " + req.getHeader("User-Agent") + " From: " + req.remoteAddr + " To: " + req.requestURL.toString() + "?" + req.queryString)

                    ClientHolder.addSuccessRequest(client, duration)
                    return


                } catch (e: Exception) {
                    e.printStackTrace()
                    res.status = HttpStatus.REQUEST_TIMEOUT.value()
                    res.writer.write("{\"message\":\"Request timeout. Unknown error\"}")
                    res.flushBuffer()
                    Singletons.requestInstance.remove(reqId)
                    Singletons.buildSocketChunkData.remove(reqId)
                    Singletons.responseQue.remove(reqId)
                    val endTime = System.nanoTime()
                    val duration = (endTime - startTime).toDouble() / 1000000 //divide by 1000000 to get milliseconds.
                    LogManager.getLogger("client-$client")
                        .info("Request failed, Unknown error. Time: $duration  UA: " + req.getHeader("User-Agent") + " From: " + InetAddress.getLocalHost().hostAddress + " To: " + req.requestURL.toString() + "?" + req.queryString)
                    ClientHolder.addFailedRequest(client)
                    return
                }
            }
        } else {
            chain.doFilter(request, response)
            return
        }
    }

    fun isResourceAvailable(path: String, itr: Int = 1): String? {
        val splPath = path.split("/").takeLast(itr)
        val f = File("web")
        if (splPath.size >= itr && f.isDirectory) {
            val search = splPath.joinToString("/")
            return f.listFiles()?.filter { it.isDirectory }?.map { it.name }?.map {
                Pair(it, resourceLoader.getResource("file:web/$it/$search"))
            }?.filter { it.second.exists() && it.second.file.isFile }?.map { "${it.first}/$search" }?.firstOrNull()
                ?: isResourceAvailable(path, itr + 1)
        }
        return null
    }
}

class RequestBuilder(
    val id: String,
    val path: String,
    val method: HttpMethod,
    var headers: MutableMap<String, String>
) : Serializable {
    var body: String? = null
    var queryString: String? = null
    var parts: MutableList<FilePart>? = null
}

class RequestQue(val id: String, val requestBuilder: RequestBuilder, var responseObj: Resp?)

class PartialData(val id: String, val total: Int, val idx: Int, val data: String, val startTime: Long)

class FilePart(val name: String, val fileName: String, val data: String)

class Resp(val body: ByteArray?, val statusCode: Int, val headers: Map<String, Array<String>>? = null)

object Singletons {
    //    val sendQueue: ConcurrentMap<String, MessageHandler> by lazy { ConcurrentHashMap<String, MessageHandler>() }
    val responseQue: ConcurrentMap<String, Resp> by lazy { ConcurrentHashMap() }
    val requestInstance: ConcurrentMap<String, RequestQue> by lazy { ConcurrentHashMap() }
    val buildSocketChunkData: ConcurrentMap<String, ChunkCollector> by lazy { ConcurrentHashMap() }

    private val properties by lazy { Properties() }
    fun properties(): Properties {

        if (properties.isEmpty) {
            val iniFIle = File("conf/general.ini")
            properties["server.port"] = when {
                System.getenv("PORT") != null -> {
                    System.getenv("PORT")
                }
                iniFIle.exists() -> {
                    val ini = Wini(iniFIle)
                    ini.get("server", "port") ?: "9090"
                }
                else -> {
                    "9090"
                }
            }
        }
        return properties
    }
}



