/**
 *  Copyright (C) 2021 Anthony Chomienne
 *  This program is free software: you can redistribute it and/or modify it under the terms of the
 *  GNU Affero General Public License as published by the Free Software Foundation, version 3.
 *
 *  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 Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License along with this program.
 *  If not, see <https://www.gnu.org/licenses/>
 */

package fr.mobdev.peertubelive.manager

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import fr.mobdev.peertubelive.BuildConfig
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.objects.OAuthData
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.net.UnknownHostException
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.collections.ArrayList

class OAuthManager {

    constructor() {
        start()
    }

    fun register(context: Context, url: String, listener: InstanceManager.InstanceListener) {
        val args = Bundle()
        args.putString(URL, url)

        val message = Message()
        message.type = Message.Message_Type.REGISTER
        message.context = context
        message.args = args
        message.listener = listener

        addMessage(message)
    }

    fun getUserToken(context: Context, url: String, username: String, password: String, oauthData: OAuthData, listener: InstanceManager.InstanceListener) {
        val args = Bundle()
        args.putString(URL, url)
        args.putParcelable(OAUTH_DATA, oauthData)
        args.putString(USERNAME, username)
        args.putString(PASSWORD, password)

        val message = Message()
        message.type = Message.Message_Type.GET_USER_TOKEN
        message.context = context
        message.args = args
        message.listener = listener

        addMessage(message)
    }

    fun refreshToken(context: Context, url: String, oauthData: OAuthData, listener: InstanceManager.InstanceListener) {
        val args = Bundle()
        args.putString(URL, url)
        args.putParcelable(OAUTH_DATA, oauthData)

        val message = Message()
        message.type = Message.Message_Type.REFRESH_TOKEN
        message.context = context
        message.args = args
        message.listener = listener

        addMessage(message)
    }

    fun post(context: Context, url: String, oauthData: OAuthData, data: Bundle, listener: InstanceManager.InstanceListener) {
        val args = Bundle()
        args.putString(URL, url)
        args.putParcelable(OAUTH_DATA, oauthData)
        args.putBundle(DATA, data)

        val message = Message()
        message.type = Message.Message_Type.POST
        message.context = context
        message.args = args
        message.listener = listener

        addMessage(message)
    }

    fun get(context: Context, url: String, oauthData: OAuthData?, listener: InstanceManager.InstanceListener) {
        val args = Bundle()
        args.putString(URL, url)
        args.putParcelable(OAUTH_DATA, oauthData)

        val message = Message()
        message.type = Message.Message_Type.GET
        message.context = context
        message.args = args
        message.listener = listener

        addMessage(message)
    }

    private companion object OAuthThread : Thread() {
        private  val messageQueue: ArrayList<Message> = ArrayList()
        private  val sem: Semaphore = Semaphore(0, true)

        private const val URL: String = "URL"
        private const val USERNAME: String = "USERNAME"
        private const val PASSWORD: String = "PASSWORD"
        private const val OAUTH_DATA: String = "OAUTH_DATA"
        private const val DATA: String = "DATA"
        private const val EXTRA_DATA: String = "EXTRA_DATA"
        private const val CONTENT_TYPE: String = "CONTENT_TYPE"
        private const val CONTENT_DATA: String = "CONTENT_DATA"

        fun addMessage(message: Message)
        {
            messageQueue.add(message)
            sem.release()
        }

        override fun run() {
            var isRunning = true
            while (isRunning) {
                try {
                    sem.acquire()
                    val mes: Message = messageQueue.removeAt(0)
                    when (mes.type) {
                        Message.Message_Type.REGISTER -> register(mes)
                        Message.Message_Type.GET_USER_TOKEN -> getUserToken(mes)
                        Message.Message_Type.REFRESH_TOKEN -> refreshToken(mes)
                        Message.Message_Type.POST -> post(mes)
                        Message.Message_Type.GET -> get(mes)
                        else -> {}
                    }
                } catch (e: InterruptedException) {
                    isRunning = false
                }
            }
        }

        fun register(message: Message) {
            if (!isConnectedToInternet(message.context)) {
                message.listener?.onError(message.context.getString(R.string.network_error))
                return
            }
            val url: String = message.args.getString(URL,"")
            val registerUrl = URL(url)
            val connection: HttpURLConnection = registerUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "GET"
            connection.setRequestProperty("User-Agent", message.context.getString(R.string.app_name))
            connection.setRequestProperty("Content-Type", "application/json")
            connection.setRequestProperty("Accept", "application/json")
            connection.doInput = true

            var inputStream: InputStream
            var inError = false
            try {
                inputStream = connection.inputStream
            } catch (e: UnknownHostException) {
                message.listener?.onError(message.context.getString(R.string.unknown_host))
                return
            } catch (e : Exception) {
                e.printStackTrace()
                inputStream = connection.errorStream
                inError = true
            }

            val response = readInputStream(inputStream)
            if(!inError) {
                if(response.isNotEmpty()) {
                    val rootObj = JSONObject(response)

                    val clientId = rootObj.getString("client_id")
                    val clientSecret = rootObj.getString("client_secret")

                    val oauthData = OAuthData(null, null, clientId, clientSecret, null, null, 0, null,0)
                    val result = Bundle()
                    result.putParcelable(EXTRA_DATA, oauthData)

                    message.listener?.onSuccess(result)
                } else {
                    message.listener?.onError(message.context.getString(R.string.unknwon_error))
                }
            } else {
                handleError(message,response)
            }
        }

        fun getUserToken(message: Message) {
            if (!isConnectedToInternet(message.context)) {
                message.listener?.onError(message.context.getString(R.string.network_error))
                return
            }
            val url: String = message.args.getString(URL, "")
            val getUserTokenUrl = URL(url)
            val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "POST"
            connection.setRequestProperty("User-Agent", message.context.getString(R.string.app_name))
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            connection.setRequestProperty("Accept", "application/json")
            connection.doInput = true
            connection.doOutput = true

            val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
            if (BuildConfig.DEBUG && oauth == null) {
                error("Missing OAUTH DATA")
            }
            var username: String = message.args.getString(USERNAME,"")
            var password: String = message.args.getString(PASSWORD,"")

            username = URLEncoder.encode(username,"UTF-8")
            password = URLEncoder.encode(password,"UTF-8")
            println(password)

            var output = ""
            output += "client_id=" + (oauth?.clientId ?: "")
            output += "&client_secret=" + (oauth?.clientSecret ?: "")
            output += "&grant_type=password"
            output += "&response_type=code"
            output += "&username=$username"
            output += "&password=$password"

            val outputStream = connection.outputStream
            outputStream.write(output.toByteArray())

            var inputStream: InputStream
            var inError = false
            try {
                inputStream = connection.inputStream
            } catch (e: UnknownHostException) {
                message.listener?.onError(message.context.getString(R.string.unknown_host))
                return
            } catch (e : Exception) {
                e.printStackTrace()
                inputStream = connection.errorStream
                inError = true
            }

            val response = readInputStream(inputStream)

            if(!inError) {
                if(response.isNotEmpty()) {
                    val rootObj = JSONObject(response)

                    val accessToken = rootObj.getString("access_token")
                    val tokenType = rootObj.getString("token_type")
                    var expires = Calendar.getInstance().timeInMillis
                    expires += rootObj.getLong("expires_in")*1000

                    val refreshToken = rootObj.getString("refresh_token")
                    var refreshTokenExpires = Calendar.getInstance().timeInMillis
                    refreshTokenExpires += rootObj.getLong("refresh_token_expires_in")*1000

                    val oauthData = OAuthData(oauth?.baseUrl,username,oauth?.clientId, oauth?.clientSecret, accessToken, tokenType, expires, refreshToken, refreshTokenExpires)
                    val result = Bundle()
                    result.putParcelable(EXTRA_DATA, oauthData)

                    message.listener?.onSuccess(result)
                } else {
                    message.listener?.onError(message.context.getString(R.string.unknwon_error))
                }
            } else {
                handleError(message,response)
            }

        }

        fun refreshToken(message: Message) {
            if (!isConnectedToInternet(message.context)) {
                message.listener?.onError(message.context.getString(R.string.network_error))
                return
            }
            val url: String = message.args.getString(URL, "")
            val getUserTokenUrl = URL(url)
            val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "POST"
            connection.setRequestProperty("User-Agent", message.context.getString(R.string.app_name))
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            connection.setRequestProperty("Accept", "application/json")
            connection.doInput = true
            connection.doOutput = true

            val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
            if (BuildConfig.DEBUG && oauth == null) {
                error("Missing OAUTH DATA")
            }

            var output = ""
            output += "client_id=" + (oauth?.clientId ?: "")
            output += "&client_secret=" + (oauth?.clientSecret ?: "")
            output += "&grant_type=refresh_token"
            output += "&response_type=code"
            output += "&refresh_token=" +(oauth?.refreshToken?: "")

            val outputStream = connection.outputStream
            outputStream.write(output.toByteArray())

            var inputStream: InputStream
            var inError = false
            try {
                inputStream = connection.inputStream
            } catch (e: UnknownHostException) {
                message.listener?.onError(message.context.getString(R.string.unknown_host))
                return
            } catch (e : Exception) {
                e.printStackTrace()
                inputStream = connection.errorStream
                inError = true
            }

            val response = readInputStream(inputStream)

            if(!inError) {
                if(response.isNotEmpty()) {
                    val rootObj = JSONObject(response)

                    val accessToken = rootObj.getString("access_token")
                    val tokenType = rootObj.getString("token_type")
                    var expires = Calendar.getInstance().timeInMillis
                    expires += rootObj.getLong("expires_in")*1000
                    val refreshToken = rootObj.getString("refresh_token")
                    var refreshTokenExpires = Calendar.getInstance().timeInMillis
                    refreshTokenExpires += rootObj.getLong("refresh_token_expires_in")*1000

                    val oauthData = OAuthData(oauth?.baseUrl,oauth?.username,oauth?.clientId, oauth?.clientSecret, accessToken, tokenType, expires, refreshToken, refreshTokenExpires)
                    val result = Bundle()
                    result.putParcelable(EXTRA_DATA, oauthData)

                    message.listener?.onSuccess(result)
                } else {
                    message.listener?.onError(message.context.getString(R.string.unknwon_error))
                }
            } else {
                handleError(message,response)
            }

        }

        fun post(message: Message) {
            if (!isConnectedToInternet(message.context)) {
                message.listener?.onError(message.context.getString(R.string.network_error))
                return
            }
            val url: String = message.args.getString(URL, "")
            val postUrl = URL(url)
            val data = message.args.getBundle(DATA)!!
            val connection: HttpURLConnection = postUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "POST"
            connection.setRequestProperty("User-Agent", message.context.getString(R.string.app_name))
            connection.setRequestProperty("Content-Type", data.getString(CONTENT_TYPE,"application/json"))
            connection.setRequestProperty("Accept", "application/json")
            connection.doInput = true
            connection.doOutput = true

            val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
            if (BuildConfig.DEBUG && oauth == null) {
                error("Missing OAUTH DATA")
            }
            val extraData: String = data.getString(CONTENT_DATA,"")
            connection.setRequestProperty("Authorization","Bearer ${oauth?.accessToken}")

            var inputStream: InputStream? = null
            val outputStream: OutputStream
            var inError = false
            try {
                outputStream = connection.outputStream
                outputStream.write(extraData.toByteArray())
                outputStream.flush()
                outputStream.close()
            } catch (e: UnknownHostException) {
                message.listener?.onError(message.context.getString(R.string.unknown_host))
                return
            } catch (e : Exception) {
                e.printStackTrace()
                inError = true
                inputStream = connection.errorStream
            }


            if(!inError) {
                try {
                    inputStream = connection.inputStream
                } catch (e: UnknownHostException) {
                    message.listener?.onError(message.context.getString(R.string.unknown_host))
                    return
                } catch (e: Exception) {
                    e.printStackTrace()
                    inputStream = connection.errorStream
                    inError = true
                }
            }
            if (inputStream != null) {
                val response = readInputStream(inputStream)
                if (!inError) {
                    if (response.isNotEmpty()) {
                        val result = Bundle()
                        result.putString(EXTRA_DATA, response)
                        message.listener?.onSuccess(result)
                    } else {
                        message.listener?.onError(message.context.getString(R.string.unknwon_error))
                    }
                } else {
                    handleError(message,response)
                }
            }
        }

        fun get(message: Message) {
            if (!isConnectedToInternet(message.context)) {
                message.listener?.onError(message.context.getString(R.string.network_error))
                return
            }
            val url: String = message.args.getString(URL, "")
            val getUserTokenUrl = URL(url)
            val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "GET"
            connection.setRequestProperty("User-Agent", message.context.getString(R.string.app_name))
            connection.setRequestProperty("Content-Type", "application/json")
            connection.setRequestProperty("Accept", "application/json")

            connection.doInput = true

            val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
            if (oauth != null) {
                connection.setRequestProperty("Authorization", "Bearer ${oauth.accessToken}")
            }

            var inputStream: InputStream
            var inError = false
            try {
                inputStream = connection.inputStream
            } catch (e: UnknownHostException) {
                message.listener?.onError(message.context.getString(R.string.unknown_host))
                return
            } catch (e : Exception) {
                e.printStackTrace()
                inputStream = connection.errorStream
                inError = true
            }

            val response = readInputStream(inputStream)
            if(!inError) {
                if(response.isNotEmpty()) {
                    val result = Bundle()
                    result.putString(EXTRA_DATA, response)
                    message.listener?.onSuccess(result)
                } else {
                    message.listener?.onError(message.context.getString(R.string.unknwon_error))
                }
            } else {
                handleError(message,response)
            }
        }

        private fun handleError(message: Message, response: String) {
            if(response.isNotEmpty()) {
                try {
                    val rootObj = JSONObject(response)
                    val error = rootObj.getString("error")
                    message.listener?.onError(error)
                } catch (e: Exception) {
                    message.listener?.onError(message.context.getString(R.string.json_error))
                }
            } else {
                message.listener?.onError(message.context.getString(R.string.unknwon_error))
            }
        }


        fun readInputStream(inputStream: InputStream) : String {
            val inReader = InputStreamReader(inputStream)
            val bufReader = BufferedReader(inReader)
            var line: String?
            var response = ""

            do {
                line = bufReader.readLine()
                if(line != null)
                    response += line

            }while (line != null)

            return response
        }

        private fun isConnectedToInternet(context: Context): Boolean {
            //verify the connectivity
            val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val network = connectivityManager.activeNetwork
                val capabilities = connectivityManager.getNetworkCapabilities(network)
                if(capabilities != null)
                    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
            } else {
                val networkInfo = connectivityManager.activeNetworkInfo
                if (networkInfo != null)
                    return networkInfo.isConnected
            }
            return false
        }
    }

    private class Message {
        var type: Message_Type = Message_Type.UNKNOWN
        var args = Bundle()
        var listener: InstanceManager.InstanceListener? = null
        lateinit var context: Context

        enum class Message_Type {
            REGISTER, GET_USER_TOKEN, REFRESH_TOKEN, POST, GET, UNKNOWN
        }
    }
}