package eu.brassepc.fitnessdroid.data import kotlinx.serialization.Serializable import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.put @Serializable data class ActivityType( val id: Int, val key: String, val nameSv: String, val nameEn: String = "", val met: Double = 4.0, val category: String = "OTHER", val isDistanceBased: Boolean = false, val isCardio: Boolean = false, val iconKey: String? = null, ) @Serializable data class Activity( val id: String, val startedAt: String, val durationSeconds: Int, val distanceMeters: Double? = null, val elevationGainMeters: Double? = null, val estimatedKcal: Double? = null, val rpe: Double? = null, val source: String = "manual", val notes: String? = null, val activityType: ActivityType, ) @Serializable data class DailySummary( val gymKcal: Double = 0.0, val activityKcal: Double = 0.0, val activeKcal: Double = 0.0, val gymSessionCount: Int = 0, val activityCount: Int = 0, val distanceMeters: Double = 0.0, val bmrKcalPerDay: Double? = null, val phoneKcal: Double? = null, val steps: Int? = null, ) @Serializable data class GoalProgress( val id: Int, val metric: String, val period: String, val targetValue: Double, val currentValue: Double, ) @Serializable data class Profile( val username: String, val displayName: String? = null, val email: String? = null, val bodyWeightKg: Double? = null, val heightCm: Double? = null, val birthYear: Int? = null, /** "male"/"female" — serverns konvention */ val sex: String? = null, ) /** Autentiserade anrop mot gym-API:t. Query-strängarna speglar gymTrackerApi.js. */ class GymApi( private val client: GraphQlClient, private val auth: AuthRepository, ) { suspend fun myProfile(): Profile { // Fallback till gamla fältuppsättningen mot en äldre server. val data = try { client.execute(MY_PROFILE, token = auth.bearerToken()) } catch (e: GraphQlException) { client.execute(MY_PROFILE_LEGACY, token = auth.bearerToken()) } val profile = data["myProfile"]?.jsonObject ?: throw GraphQlException(listOf("Kunde inte hämta profilen")) return client.json.decodeFromJsonElement(profile) } /** Kroppsdata till servern. null = rör ej, -1/"" = rensa (serverns konvention). */ suspend fun updateProfile(heightCm: Double?, birthYear: Int?, sex: String?): Profile { val data = client.execute( "mutation(\$h: Float,\$y: Int,\$s: String){updateProfile(heightCm:\$h,birthYear:\$y,sex:\$s){username displayName email bodyWeightKg heightCm birthYear sex}}", buildJsonObject { heightCm?.let { put("h", it) } birthYear?.let { put("y", it) } sex?.let { put("s", it) } }, auth.bearerToken(), ) val profile = data["updateProfile"]?.jsonObject ?: throw GraphQlException(listOf("Kunde inte spara kroppsdata")) return client.json.decodeFromJsonElement(profile) } /* ---------- Aktiviteter ---------- */ suspend fun activityTypes(): List { val data = client.execute( "query{activityTypes{id key nameSv nameEn met category isDistanceBased isCardio iconKey}}", token = auth.bearerToken(), ) return data["activityTypes"]!!.jsonArray.map { client.json.decodeFromJsonElement(it) } } suspend fun activities(limit: Int = 50): List { val data = client.execute( "query(\$l:Int){activities(limit:\$l){$ACTIVITY_FIELDS}}", buildJsonObject { put("l", limit) }, auth.bearerToken(), ) return data["activities"]!!.jsonArray.map { client.json.decodeFromJsonElement(it) } } /** * Logga aktivitet. Servern beräknar kcal (MET/tempo/RPE) om estimatedKcal * utelämnas. null = utelämna fält. */ suspend fun addActivity( activityTypeId: Int, startedAtIso: String, durationSeconds: Int, distanceMeters: Double? = null, elevationGainMeters: Double? = null, rpe: Double? = null, estimatedKcal: Double? = null, source: String? = null, routePolyline: String? = null, notes: String? = null, ): Activity { val data = client.execute( "mutation(\$input: AddActivityInput!){addActivity(input:\$input){$ACTIVITY_FIELDS}}", buildJsonObject { put("input", buildJsonObject { put("activityTypeId", activityTypeId) put("startedAt", startedAtIso) put("durationSeconds", durationSeconds) distanceMeters?.let { put("distanceMeters", it) } elevationGainMeters?.let { put("elevationGainMeters", it) } rpe?.let { put("rpe", it) } estimatedKcal?.let { put("estimatedKcal", it) } source?.let { put("source", it) } routePolyline?.let { put("routePolyline", it) } notes?.let { put("notes", it) } }) }, auth.bearerToken(), ) return client.json.decodeFromJsonElement(data["addActivity"]!!) } /** null = rör ej, -1/"" = rensa (serverns konvention). */ suspend fun updateActivity( id: String, activityTypeId: Int? = null, startedAtIso: String? = null, durationSeconds: Int? = null, distanceMeters: Double? = null, elevationGainMeters: Double? = null, rpe: Double? = null, notes: String? = null, ): Activity { val data = client.execute( "mutation(\$input: UpdateActivityInput!){updateActivity(input:\$input){$ACTIVITY_FIELDS}}", buildJsonObject { put("input", buildJsonObject { put("id", id) activityTypeId?.let { put("activityTypeId", it) } startedAtIso?.let { put("startedAt", it) } durationSeconds?.let { put("durationSeconds", it) } distanceMeters?.let { put("distanceMeters", it) } elevationGainMeters?.let { put("elevationGainMeters", it) } rpe?.let { put("rpe", it) } notes?.let { put("notes", it) } }) }, auth.bearerToken(), ) return client.json.decodeFromJsonElement(data["updateActivity"]!!) } suspend fun deleteActivity(id: String) { client.execute( "mutation(\$id: UUID!){deleteActivity(id:\$id)}", buildJsonObject { put("id", id) }, auth.bearerToken(), ) } /** Dagens sammanställning; [dayStartIso] = lokala midnatt som instant. */ suspend fun dailySummary(dayStartIso: String): DailySummary { val data = client.execute( "query(\$d:DateTime){dailySummary(date:\$d){gymKcal activityKcal activeKcal gymSessionCount activityCount distanceMeters bmrKcalPerDay phoneKcal steps}}", buildJsonObject { put("d", dayStartIso) }, auth.bearerToken(), ) return client.json.decodeFromJsonElement(data["dailySummary"]!!) } /* ---------- Mål ---------- */ /** Mål med progress; klienten skickar sina lokala periodstarter som instants. */ suspend fun goalsWithProgress( dayStartIso: String, weekStartIso: String, monthStartIso: String, ): List { val data = client.execute( "query(\$d:DateTime!,\$w:DateTime!,\$m:DateTime!){goalsWithProgress(dayStart:\$d,weekStart:\$w,monthStart:\$m){id metric period targetValue currentValue}}", buildJsonObject { put("d", dayStartIso); put("w", weekStartIso); put("m", monthStartIso) }, auth.bearerToken(), ) return data["goalsWithProgress"]!!.jsonArray.map { client.json.decodeFromJsonElement(it) } } /** targetValue <= 0 tar bort målet. */ suspend fun setGoal(metric: String, period: String, targetValue: Double) { client.execute( "mutation(\$m:String!,\$p:String!,\$t:Float!){setGoal(metric:\$m,period:\$p,targetValue:\$t)}", buildJsonObject { put("m", metric); put("p", period); put("t", targetValue) }, auth.bearerToken(), ) } companion object { private const val ACTIVITY_FIELDS = "id startedAt durationSeconds distanceMeters elevationGainMeters estimatedKcal rpe source notes " + "activityType{id key nameSv nameEn met category isDistanceBased isCardio iconKey}" private const val MY_PROFILE = "query{myProfile{username displayName email bodyWeightKg heightCm birthYear sex}}" private const val MY_PROFILE_LEGACY = "query{myProfile{username displayName email bodyWeightKg}}" } }