package de.sebse.fuplanner2.blackboard

import android.content.Context
import com.android.volley.VolleyError
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Parser
import de.sebse.fuplanner2.auth.FUAuthModule.Companion.REFRESH_FREQUENCY_COURSES_MILLIS
import de.sebse.fuplanner2.database.*
import de.sebse.fuplanner2.network.Requester
import de.sebse.fuplanner2.utils.UpdateResult
import de.sebse.fuplanner2.utils.pmap
import de.sebse.fuplanner2.utils.updateResultOf
import de.sebse.fuplanner2.whiteboard.Whiteboard
import de.sebse.fuplanner2.worker.FetchResourceErrorType
import de.sebse.fuplanner2.worker.FetchResourceException
import java.lang.StringBuilder

suspend fun Blackboard.getCourseByLocationRef(stored: List<Course>, requester: Requester, user: User, blockedLvNumbers: Set<String>, database: AppDatabase, locationRef: String): Course? {
    val course = stored.find { it.internalId == locationRef }?.apply {
        if (lastRefreshed > System.currentTimeMillis() - REFRESH_FREQUENCY_COURSES_MILLIS)
            return this
    }
    return getCourseByCourse(requester, user, blockedLvNumbers, database, locationRef, course)
}

suspend fun Blackboard.getCourseByCourse(requester: Requester, user: User, blockedLvNumbers: Set<String>, database: AppDatabase, locationRef: String, course: Course?): Course? {

    val userId = user.uid ?: return null

    val jsonSite = Cache.getCache(database, locationRef, "Blackboard_Courses", REFRESH_FREQUENCY_COURSES_MILLIS) {
        requester.get(
            FETCH_COURSE_DETAILS.format(locationRef),
            getCookies(user)
        ).body
    }.let { Parser.default().parse(StringBuilder(it)) as JsonObject }

    val uid = course?.uid
    val moduleType = course?.moduleType ?: MODULE_TYPE
    val (isSummerSemester, year) = jsonSite.string("courseId")?.let { courseId ->
        val groups = Regex("([0-9]{2})([WS])\$").find(courseId)?.groupValues
        val type: String? = groups?.getOrNull(2)
        val year: String? = groups?.getOrNull(1)
        val isSS = type == "S"
        val yearInt = if (type != null && year != null) year.toIntOrNull(10) else null
        Pair(isSS, yearInt)
    } ?: Pair(false, null)
    val (lvNumbers, type) = jsonSite.string("courseId")?.let { courseId ->
        val groups = Regex("^[A-Z0-9a-z-]+_([A-Z0-9a-z-]*)_([A-Z0-9a-z-]+)_[0-9]{2}[WS]\$").find(courseId)?.groupValues
        val type: String? = groups?.getOrNull(1)
        val lv: String? = groups?.getOrNull(2)
        Pair(if (lv == null) setOf() else setOf(lv), if (type.isNullOrEmpty()) "Sonstiges" else type)
    } ?: Pair(setOf(), "Sonstiges")
    if (lvNumbers.intersect(blockedLvNumbers).any()) return null
    val lecturers = this.requestList(
        requester, user, FETCH_COURSE_USERS.format(locationRef),
        {json -> json.array("results")},
        {json -> json.obj("paging")?.string("nextPage")}
    ).filter { it.string("courseRoleId") == "Instructor" && it.string("userId")?.isNotEmpty() ?: false }
        .map { lecturer ->
            val lecturerUserId = lecturer.string("userId") ?: return@map null
            Cache.getCache(database, lecturerUserId, "Blackboard_Users", REFRESH_FREQUENCY_COURSES_MILLIS) {
                requester.get(
                    FETCH_USER_DETAILS.format(lecturerUserId),
                    getCookies(user)
                ).body
            }.let { Parser.default().parse(StringBuilder(it)) as JsonObject }
            .let { Lecturer(
                it.obj("name")?.string("given") ?: "",
                it.obj("name")?.string("family") ?: "",
                it.string("userName") + "@zedat.fu-berlin.de",
                true
            ) }
        }.filterNotNull()
    return Course(
        uid,
        userId,
        System.currentTimeMillis(),
        isSummerSemester,
        year,
        lvNumbers.toHashSet(),
        jsonSite.string("displayName") ?: "",
        type,
        jsonSite.string("description") ?: "",
        locationRef,
        moduleType,
        lecturers
    )
}

suspend fun Blackboard.getCourses(ctx: Context, database: AppDatabase, user: User): UpdateResult<Course> {

    if (!isAvailable(user)) return UpdateResult()
    val userId = user.uid ?: return UpdateResult()

    val requester = Requester(ctx)
    val stored = database.courseDao().getAllByType(userId, MODULE_TYPE)
    val blockedLvNumbers = database.courseDao()
        .getAllByType(userId, Whiteboard.MODULE_TYPE)
        .map { it.lvNumber.toSet() }
        .reduce { a, b -> a.union(b) }

    try {
        val data = this.requestList(
            requester, user, FETCH_COURSE_MEMBERSHIP.format(user.bbInfo.id),
            {json -> json.array("results")},
            {json -> json.obj("paging")?.string("nextPage")}
        )
        val new = data.mapNotNull { it.string("courseId") }.pmap {
            getCourseByLocationRef(stored, requester, user, blockedLvNumbers, database, locationRef = it)
        }.filterNotNull()

        val result = updateResultOf(stored, new)
        database.courseDao().run {
            delete(result.removed)
            upsert(result.values)
        }
        return result
    } catch (e: VolleyError) {
        if (e.networkResponse.statusCode == 401)
            throw FetchResourceException(
                FetchResourceErrorType.ERR_AUTHORIZATION
            )
        throw FetchResourceException(
            FetchResourceErrorType.ERR_NETWORK_ERROR
        )
    }
}

suspend fun Blackboard.getCourse(ctx: Context, database: AppDatabase, user: User, courseId: Long): UpdateResult<Course> {

    if (!isAvailable(user)) return UpdateResult()

    val requester = Requester(ctx)
    val blockedLvNumbers = database.courseDao()
        .getAllByType(user.uid!!, Whiteboard.MODULE_TYPE)
        .map { it.lvNumber.toSet() }
        .reduce { a, b -> a.union(b) }

    try {
        database.courseDao().run {
            val old = getCourseById2(courseId)
            if (old.moduleType == MODULE_TYPE) {
                getCourseByCourse(requester, user, blockedLvNumbers, database, old.internalId, old)?.also {
                    upsert(it)
                    return updateResultOf(listOf(old), listOf(it))
                }
            }
            return UpdateResult()
        }
    } catch (e: VolleyError) {
        if (e.networkResponse.statusCode == 401)
            throw FetchResourceException(
                FetchResourceErrorType.ERR_AUTHORIZATION
            )
        throw FetchResourceException(
            FetchResourceErrorType.ERR_NETWORK_ERROR
        )
    }
}