package de.sebse.fuplanner2.blackboard

import android.content.Context
import android.net.Uri
import com.beust.klaxon.JsonArray
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Parser
import de.sebse.fuplanner2.auth.FUAuthModule
import de.sebse.fuplanner2.auth.SamlReponse
import de.sebse.fuplanner2.database.User
import de.sebse.fuplanner2.network.NetData
import de.sebse.fuplanner2.network.Requester
import de.sebse.fuplanner2.network.Tools.invalidResponse
import java.net.URI
import java.util.concurrent.TimeUnit
import kotlin.random.Random

object Blackboard: FUAuthModule() {
    private const val LOGIN_URL = "https://lms.fu-berlin.de/lms-apps/login/sso/index.php"
    private const val TEST_URL = "https://lms.fu-berlin.de/learn/api/public/v1/users?userName=%s&fields=id,userName,studentId,name,contact"
    private const val RESTORE_SESSION_URI = "https://lms.fu-berlin.de"
    internal const val MODULE_TYPE = 1

    internal const val FETCH_COURSE_MEMBERSHIP = "https://lms.fu-berlin.de/learn/api/public/v1/users/%s/courses?limit=200&fields=courseId" // userId
    internal const val FETCH_COURSE_DETAILS = "https://lms.fu-berlin.de/learn/api/v1/courses/%s?fields=displayName,description,courseId,startDate,endDate" // courseId
    internal const val FETCH_COURSE_USERS = "https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/users?fields=courseRoleId,userId&limit=200" // courseId
    internal const val FETCH_USER_DETAILS = "https://lms.fu-berlin.de/learn/api/public/v1/users/%s?fields=name,userName" // lecturerId

    internal const val FETCH_VV_COURSE_ID = "https://www.fu-berlin.de/vv/de/search?utf8=✓&query=%s"
    internal const val FETCH_VV_COURSE = "https://www.fu-berlin.de/vv/de/lv/%s"
    // 14-21 days (random to reduce heap load)
    val REFRESH_FREQUENCY_EVENT_CACHE_MILLIS = TimeUnit.HOURS.toMillis(14*24) +
                Random.nextLong(TimeUnit.HOURS.toMillis(21*24))


    internal const val FETCH_ANNOUNCEMENT_LIST = "https://lms.fu-berlin.de/learn/api/v1/courses/%s/announcements?fields=id,title,body.rawText,startDateRestriction,creatorUserId" // courseId

    override suspend fun isAvailable(ctx: Context, name: String): Boolean {
        return true
    }

    override suspend fun login(ctx: Context, name: String, password: String, user: User) {
        val requester = Requester(ctx)
        var response = requester.head(LOGIN_URL, cookies = getCookies(user, shib = true))
        val samlUri = response.headers?.get("Location")
            ?: throw invalidResponse(101100, "Location header not set!")

        if (!samlUri.startsWith(RESTORE_SESSION_URI)) {
            val samlResponse: SamlReponse = doSaml(ctx, samlUri, name, password, user)

            // Shib-Session-Cookie
            response = requester.post(samlResponse.uri, cookies = null, data = hashMapOf(
                "RelayState" to samlResponse.relayState,
                "SAMLResponse" to samlResponse.samlResponse
            ))
            updateCookies(user, response)
            // Finish BB
            response = requester.get(
                response.networkResponse.headers?.get("Location") ?: throw invalidResponse(101101, "No Location header to finish Blackboard"),
                getCookies(user, shib = true)
            )
        }
        // Start Session
        response = requester.get(
            response.networkResponse.headers?.get("Location") ?: throw invalidResponse(101102, "No Location header to start Blackboard session"),
            getCookies(user, shib = true)
        )

        updateCookies(user, response)
        // Set API JSESSION
        response = requester.get(String.format(TEST_URL, Uri.encode(name)), getCookies(user, shib = true))
        updateCookies(user, response)
        user.bbInfo.id = (Parser.default().parse(StringBuilder(response.body)) as JsonObject)
                .array<JsonObject>("result")
                ?.find { entry -> entry["userName"] == name }
                ?.string("id")

        (Parser.default().parse(StringBuilder(response.body)) as JsonObject)
            .array<JsonObject>("results")
            ?.find { entry -> entry["userName"] == name }
            ?.run {
                user.bbInfo.id = string("id") ?: user.wbInfo.id
                user.email = obj("contact")?.string("email") ?: user.email
                user.matNumber = string("studentId")?.toIntOrNull() ?: user.matNumber
                user.firstName = obj("name")?.string("given") ?: user.firstName
                user.lastName = obj("name")?.string("family") ?: user.lastName
        }
    }

    private fun updateCookies(user: User, response: NetData) {
        val setCookies = response.networkResponse.allHeaders?.let { parseCookies(it) }
        setCookies?.get("JSESSIONID")?.let {
            user.cookies.bbJsessionId = it
        }
        setCookies?.get("session_id")?.let {
            user.cookies.bbSessionId = it
        }
        setCookies?.get("s_session_id")?.let {
            user.cookies.bbSSessionId = it
        }
        setCookies
            ?.filter{ (key, _) -> key.startsWith("_shibsession_") }
            ?.forEach { (key, value) ->
                user.cookies.bbShibKey = key
                user.cookies.bbShibValue = value
                return@forEach
            }
    }

    internal fun getCookies(user: User, shib: Boolean = true): HashMap<String, String>? {
        val cookies = user.cookies.bbJsessionId?.let { key -> hashMapOf("JSESSIONID" to key) } ?: hashMapOf()
        user.cookies.bbSessionId?.let { key -> cookies["session_id"] = key }
        user.cookies.bbSSessionId?.let { key -> cookies["s_session_id"] = key }
        if (shib && user.cookies.bbShibValue != null) {
            user.cookies.bbShibKey?.let { cookies[it] = user.cookies.bbShibValue ?: "" }
        }
        return cookies
    }

    internal fun isAvailable(user: User): Boolean {
        return user.cookies.bbJsessionId != null && user.cookies.bbSessionId != null && user.cookies.bbSSessionId != null
    }

    internal suspend fun requestList(
        requester: Requester, user: User,
        entryUri: String,
        arrayPath: ((JsonObject) -> JsonArray<JsonObject>?),
        nextPage: ((JsonObject) -> String?)
    ): Array<JsonObject> {
        var data: Array<JsonObject> = arrayOf()
        var url: String = entryUri
        do {
            val json = requester.get(url, getCookies(user)).let {
                Parser.default().parse(StringBuilder(it.body)) as JsonObject
            }
            val next = nextPage(json) ?: break
            if (next.isEmpty()) break
            url = URI(url).resolve(next).toString()
            val results = arrayPath(json)?.toTypedArray() ?: arrayOf()
            if (results.isEmpty()) break
            data += results
        } while (true)
        return data
    }
}