package com.ustadmobile.core.domain.xapi.state.h5puserdata

import com.benasher44.uuid.uuidFrom
import com.ustadmobile.core.db.UmAppDatabase
import com.ustadmobile.core.domain.interop.HttpApiException
import com.ustadmobile.core.domain.xapi.XapiJson
import com.ustadmobile.core.domain.xapi.http.queryParamOrThrow
import com.ustadmobile.core.domain.xapi.http.xapiStateParams
import com.ustadmobile.core.domain.xapi.model.XapiAgent
import com.ustadmobile.core.domain.xapi.model.identifierHash
import com.ustadmobile.core.domain.xapi.state.activityUid
import com.ustadmobile.core.domain.xapi.state.hash
import com.ustadmobile.xxhashkmp.XXHasher64Factory
import com.ustadmobile.xxhashkmp.XXStringHasher
import com.ustadmobile.door.util.systemTimeInMillis
import com.ustadmobile.ihttp.request.IHttpRequest
import com.ustadmobile.ihttp.request.IHttpRequestWithFormUrlEncodedData
import com.ustadmobile.ihttp.response.IHttpResponse
import com.ustadmobile.ihttp.response.StringResponse
import com.ustadmobile.lib.db.entities.xapi.StateEntity
import com.ustadmobile.lib.db.entities.xapi.XapiSessionEntity
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject

/**
 * Implementation of the UserData endpoint API for H5P, which uses the StateEntity for persistence.
 *
 * Unfortunately, the documentation for ContentUserData still shows "coming soon" as per:
 * https://h5p.org/creating-your-own-h5p-plugin
 *
 * POST
 * Used to save user content data. The request body will be url form encoded with three fields:
 *     preload: (1|0) - 1 means this content user data must be preloaded and provided to the
 *                       H5PIntegration as the contentUserData parameter. See
 *                       https://github.com/h5p/h5p-php-library/blob/4599291d7ce0cfb90edd188b181416f31514748e/js/h5p.js#L2338
 *
 *     invalidate: (1|0) - 1 means this state must be deleted if the content changes. This is
 *                          defacto always the case in the Ustad App.
 *     data: always a JSON string (the H5P.js lib uses JQuery's .ajax function and sets the dataType
 *            to Json.
 *
 *   Query parameters:
 *     dataType: this is (confusing) synonymous with dataId as used in h5p.js . It is essentially
 *               the stateId key.
 *     subContentId: content types that contain subcontent will use this. The main content id will
 *                   always be 0 as per h5p.js .
 *     contentId: the contentId a identified, in reality we use the Xapi activity id.
 *
 * GET - preload
 * Used to get all UserData that was flagged to preload, which needs to be provided as the
 * contentUserData parameter. This takes the form of an array, sorted by the subContentId. See
 * H5P-NodeJs implementation here:
 * https://github.com/Lumieducation/H5P-Nodejs-library/blob/master/packages/h5p-server/src/ContentUserDataManager.ts#L208
 * See also the h5p-standalone state.html test:
 * https://github.com/tunapanda/h5p-standalone/blob/master/test/state.html
 * e.g.
 * [
 *   //For each subContentId there is a JsonObject.
 *   {
 *       dataType: "data-as-string" //Even if the data is JSON, it will be provided as a string
 *   },
 *   ...
 * ]
 *
 * The Javascript generated by H5PContentImporter will request the userdata to preload.
 *
 * GET - load specified state on demand
 * The body is returned as follows:
 * {
 *    success: true,
 *    data: "data-as-string"
 * }
 *
 * If there is no existing state, then body would be:
 * {
 *    success: true,
 *    data: false
 * }
 *
 */
class H5PUserDataEndpointUseCase(
    private val db: UmAppDatabase,
    private val repo: UmAppDatabase?,
    private val xxStringHasher: XXStringHasher,
    private val xxHasher64Factory: XXHasher64Factory,
    xapiJson: XapiJson,
) {

    private val json = xapiJson.json

    suspend operator fun invoke(
        request: IHttpRequest,
        xapiSessionEntity: XapiSessionEntity,
    ) : IHttpResponse {
        val hasher = xxHasher64Factory.newHasher(0)
        val subContentId = request.queryParam("subContentId")

        val xapiAgent = try {
            json.decodeFromString(XapiAgent.serializer(), request.queryParamOrThrow("agent"))
        }catch(e: Throwable) {
            throw HttpApiException(400, "Agent is not valid: ${e.message}", e)
        }

        val agentActorUid = xapiAgent.identifierHash(xxStringHasher)
        val preloadParam = request.queryParam("preload")

        return when {
            //H5P saving state
            request.method == IHttpRequest.Companion.Method.POST -> {
                val xapiStateParams = request.xapiStateParams()
                val registrationUuid = xapiStateParams.registrationUuid
                val requestData = (request as IHttpRequestWithFormUrlEncodedData).bodyAsFormUrlEncodedDataMap()

                val stateEntity = StateEntity(
                    seActorUid = agentActorUid,
                    seHash = xapiStateParams.hash(hasher, h5pSubContentId = subContentId),
                    seActivityUid = xapiStateParams.activityUid(xxStringHasher),
                    seStateId = xapiStateParams.stateId,
                    seContentType = "application/json; charset=utf-8",
                    seLastMod = systemTimeInMillis(),
                    seContent = requestData["data"]?.firstOrNull()
                        ?: throw HttpApiException(400, "PUT H5P state: no data"),
                    seDeleted = false,
                    seRegistrationHi = registrationUuid?.mostSignificantBits,
                    seRegistrationLo = registrationUuid?.leastSignificantBits,
                    seH5PPreloaded = (requestData["preload"]?.firstOrNull()?.toIntOrNull() ?: 0) != 0,
                    seH5PSubContentId = subContentId,
                )

                (repo ?: db).stateEntityDao().upsertAsync(listOf(stateEntity))

                StringResponse(
                    request = request,
                    mimeType = "text/plain",
                    responseCode = 200,
                    body = ""
                )
            }

            //Get state to preload
            request.method == IHttpRequest.Companion.Method.GET && preloadParam != null -> {
                val registrationUuid = request.queryParam("registration")?.let { uuidFrom(it) }
                val stateEntities = (repo ?: db).stateEntityDao().getH5PPreload(
                    accountPersonUid = xapiSessionEntity.xseAccountPersonUid,
                    actorUid = agentActorUid,
                    seActivityUid = xxStringHasher.hash(request.queryParamOrThrow("activityId")),
                    registrationUuidHi = registrationUuid?.mostSignificantBits,
                    registrationUuidLo = registrationUuid?.leastSignificantBits
                )

                val stateForSubContent = stateEntities
                    .groupBy { (it.seH5PSubContentId ?: "0") }
                    .map { (subContentId, stateEntities) ->
                        subContentId.toFloat() to buildJsonObject {
                            stateEntities.forEach { stateEntity ->
                                put(stateEntity.seStateId, JsonPrimitive(stateEntity.seContent))
                            }
                        }
                    }

                val userDataJsonArray = JsonArray(
                    stateForSubContent.sortedBy { it.first }.map { it.second }
                )

                return StringResponse(
                    request = request,
                    mimeType = "application/json; charset=utf-8",
                    responseCode = 200,
                    body = json.encodeToString(JsonArray.serializer(), userDataJsonArray),
                )
            }

            //Get state by specified key (e.g. dataType / stateId )
            request.method == IHttpRequest.Companion.Method.GET -> {
                val xapiStateParams = request.xapiStateParams()
                val hash = xapiStateParams.hash(hasher, h5pSubContentId = subContentId)
                val stateEntity = db.stateEntityDao().findByActorAndHash(
                    accountPersonUid = xapiSessionEntity.xseAccountPersonUid,
                    actorUid = agentActorUid,
                    seHash = hash,
                    includeDeleted = false
                )

                /*
                 * Handled by the JQuery .ajax success parameter (where dataType for .ajax has been set
                 * to "json") as per
                 * https://github.com/h5p/h5p-php-library/blob/4599291d7ce0cfb90edd188b181416f31514748e/js/h5p.js#L2338
                 */
                StringResponse(
                    request = request,
                    mimeType = "application/json; charset=utf-8",
                    responseCode = 200,
                    body = json.encodeToString(
                        serializer = JsonObject.serializer(),
                        value = buildJsonObject {
                            put("success", JsonPrimitive(true))
                            if(stateEntity != null) {
                                put("data", JsonPrimitive(stateEntity.seContent))
                            }else {
                                put("data", JsonPrimitive(false))
                            }
                        }
                    )
                )
            }

            else -> {
                throw HttpApiException(400, "Method not supported: ${request.method} ${request.url}")
            }
        }
    }

}