package de.tsschulz.tt_tagebuch.app.ui

import de.tsschulz.tt_tagebuch.app.util.OfficialTournamentEligibility
import de.tsschulz.tt_tagebuch.shared.api.models.Member
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialCompetitionMemberStateDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParsedCompetitionDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParsedDataDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParsedTournamentEnvelopeDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParticipationBucketDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParticipationEntryDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialTournamentListRowDto
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import java.text.Collator
import java.util.Calendar
import java.util.Locale

internal enum class OfficialParticipationRange(val vueKey: String) {
    M3("3m"),
    M6("6m"),
    M12("12m"),
    Y2("2y"),
    PREV("prev"),
    ALL("all"),
}

internal data class ParticipationState(
    val wants: Boolean,
    val registered: Boolean,
    val participated: Boolean,
    val placement: String?,
)

internal data class ClubHistoryRow(
    val key: String,
    val memberName: String?,
    val tournamentName: String?,
    val competitionName: String?,
    val date: String?,
    val placement: String?,
)

internal data class ParticipantTableRow(
    val key: String,
    val memberId: Int,
    val memberName: String,
    val competitionName: String,
    val start: String,
    val wants: Boolean,
    val registered: Boolean,
    val participated: Boolean,
    val placement: String?,
)

internal data class ParticipantGroup(
    val memberName: String,
    val memberId: Int,
    val items: List<ParticipantTableRow>,
)

internal data class ActiveCompetitionMemberRow(
    val key: String,
    val memberName: String,
    val wants: Boolean,
    val registered: Boolean,
    val participated: Boolean,
    val placement: String?,
    val statusClass: String,
    val statusText: String,
)

internal data class CompetitionSummaryCounts(
    val interested: Int,
    val registered: Int,
    val participated: Int,
)

internal data class TournamentListStatus(
    val label: String,
    val className: String,
)

internal data class RegistrationDeadlineInfo(
    val label: String,
    val statusClass: String,
)

internal data class WorkflowNotice(
    val label: String,
    val statusClass: String,
)

internal data class CompetitionForMemberRow(
    val key: String,
    val name: String,
    val date: String,
    val time: String,
    val entryFee: String,
    val raw: OfficialParsedCompetitionDto,
)

private val collator: Collator = Collator.getInstance(Locale.GERMAN).apply { strength = Collator.PRIMARY }

internal fun participationKey(competitionId: Int, memberId: Int): String = "$competitionId-$memberId"

internal fun buildParticipationMapFromApi(entries: List<OfficialCompetitionMemberStateDto>): Map<String, ParticipationState> {
    val out = LinkedHashMap<String, ParticipationState>()
    for (e in entries) {
        val key = participationKey(e.competitionId, e.memberId)
        out[key] = ParticipationState(
            wants = e.wants,
            registered = e.registered,
            participated = e.participated,
            placement = e.placement,
        )
    }
    return out
}

internal fun getParticipation(
    map: Map<String, ParticipationState>,
    competitionId: Any,
    memberId: Any,
): ParticipationState {
    val key = "${competitionId}-${memberId}"
    return map[key] ?: ParticipationState(false, false, false, null)
}

internal fun tournamentListRawDate(t: OfficialTournamentListRowDto): String =
    (t.eventDate ?: "").trim()

internal fun tournamentDateRange(t: OfficialTournamentListRowDto): Pair<Calendar?, Calendar?> {
    val raw = tournamentListRawDate(t)
    if (raw.isEmpty()) return Pair(null, null)
    val matches = Regex("(\\d{1,2}\\.\\d{1,2}\\.\\d{4}|\\d{4}-\\d{2}-\\d{2})").findAll(raw).mapNotNull { m ->
        OfficialTournamentEligibility.parseDateFlexible(m.value)
    }.sortedBy { it.timeInMillis }.toList()
    if (matches.isEmpty()) return Pair(null, null)
    return Pair(matches.first(), matches.last())
}

internal fun tournamentListStatus(selectedId: Int?, t: OfficialTournamentListRowDto): TournamentListStatus {
    if (selectedId != null && selectedId == t.id) {
        return TournamentListStatus("Aktiv", "is-active")
    }
    val range = tournamentDateRange(t)
    val date = range.second ?: range.first
    if (date == null) return TournamentListStatus("Ohne Datum", "is-neutral")
    val today = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, 0)
        set(Calendar.MINUTE, 0)
        set(Calendar.SECOND, 0)
        set(Calendar.MILLISECOND, 0)
    }
    return if (date.timeInMillis < today.timeInMillis) {
        TournamentListStatus("Vergangen", "is-past")
    } else {
        TournamentListStatus("Kommend", "is-upcoming")
    }
}

internal fun filterTournamentList(
    list: List<OfficialTournamentListRowDto>,
    tournamentSearch: String,
    showOlderTournaments: Boolean,
    selectedTournamentId: Int?,
): List<OfficialTournamentListRowDto> {
    val search = tournamentSearch.trim().lowercase(Locale.ROOT)
    val searchDate = if (search.isNotEmpty()) OfficialTournamentEligibility.parseDateFlexible(search) else null
    val today = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, 0)
        set(Calendar.MINUTE, 0)
        set(Calendar.SECOND, 0)
        set(Calendar.MILLISECOND, 0)
    }
    val cutoff = Calendar.getInstance().apply {
        timeInMillis = today.timeInMillis
        add(Calendar.DAY_OF_MONTH, -14)
    }

    val base = list.filter { tournament ->
        val isSelected = selectedTournamentId != null && selectedTournamentId == tournament.id
        val range = tournamentDateRange(tournament)
        val end = range.second ?: range.first
        if (!showOlderTournaments && end != null && end.timeInMillis < cutoff.timeInMillis && !isSelected) {
            return@filter false
        }
        if (search.isEmpty()) return@filter true
        if (searchDate != null) {
            val start = range.first ?: return@filter false
            val endDate = range.second ?: range.first
            return@filter !(searchDate.timeInMillis < start.timeInMillis || searchDate.timeInMillis > endDate!!.timeInMillis)
        }
        val haystack = listOfNotNull(tournament.title, tournament.eventDate).joinToString(" ").lowercase(Locale.ROOT)
        return@filter haystack.contains(search)
    }

    return base.sortedWith { a, b ->
        val ra = tournamentDateRange(a)
        val rb = tournamentDateRange(b)
        val ta = (ra.second ?: ra.first)?.timeInMillis ?: 0L
        val tb = (rb.second ?: rb.first)?.timeInMillis ?: 0L
        compareValues(tb, ta)
    }
}

internal fun jsonElementToStringList(el: JsonElement?): List<String> {
    if (el == null || el is JsonNull) return emptyList()
    if (el is JsonArray) {
        return el.mapNotNull { item ->
            when (item) {
                is JsonPrimitive -> item.contentOrNull
                else -> item.toString()
            }
        }
    }
    if (el is JsonPrimitive && el.isString) return listOf(el.content)
    return emptyList()
}

internal fun tournamentLocationLabel(parsed: OfficialParsedDataDto?): String {
    val places = jsonElementToStringList(parsed?.austragungsorte)
    val line = places.firstOrNull { Regex("^PLZ\\s*/\\s*Ort:", RegexOption.IGNORE_CASE).containsMatchIn(it) }
        ?: return ""
    return line.replaceFirst(Regex("^PLZ\\s*/\\s*Ort:", RegexOption.IGNORE_CASE), "").trim()
}

internal fun parseDateTimeFlexible(s: String?): Calendar? {
    if (s.isNullOrBlank()) return null
    val t = s.trim()
    val m = Regex("^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:\\s+(\\d{1,2}):(\\d{2}))?$").find(t)
    if (m != null) {
        val day = m.groupValues[1].toInt()
        val month = m.groupValues[2].toInt()
        val year = m.groupValues[3].toInt()
        val hh = m.groupValues[4].ifEmpty { "0" }.toInt()
        val mm = m.groupValues[5].ifEmpty { "0" }.toInt()
        return Calendar.getInstance().apply {
            set(year, month - 1, day, hh, mm, 0)
            set(Calendar.MILLISECOND, 0)
        }
    }
    return OfficialTournamentEligibility.parseDateFlexible(t)
}

internal fun registrationDeadlineAt(parsed: OfficialParsedDataDto?): Calendar? {
    if (parsed == null) return null
    val deadlines = mutableListOf<String>()
    deadlines.addAll(jsonElementToStringList(parsed.meldeschluesse))
    for (c in parsed.competitions) {
        listOfNotNull(
            c.registrationDeadlineDate,
            c.registrationDeadlineOnline,
            c.meldeschlussDatum,
            c.meldeschlussOnline,
        ).forEach { if (!it.isNullOrBlank()) deadlines.add(it) }
    }
    var earliest: Calendar? = null
    for (entry in deadlines) {
        val p = parseDateTimeFlexible(entry) ?: continue
        if (earliest == null || p.timeInMillis < earliest.timeInMillis) earliest = p
    }
    return earliest
}

internal fun registrationDeadlineInfo(parsed: OfficialParsedDataDto?): RegistrationDeadlineInfo {
    val at = registrationDeadlineAt(parsed)
    if (at == null) {
        return RegistrationDeadlineInfo("Meldeschluss unbekannt", "is-neutral")
    }
    var earliestText = ""
    val deadlines = mutableListOf<String>()
    parsed?.let { p ->
        deadlines.addAll(jsonElementToStringList(p.meldeschluesse))
        for (c in p.competitions) {
            listOfNotNull(
                c.registrationDeadlineDate,
                c.registrationDeadlineOnline,
                c.meldeschlussDatum,
                c.meldeschlussOnline,
            ).forEach { if (!it.isNullOrBlank()) deadlines.add(it) }
        }
    }
    for (entry in deadlines) {
        val p = parseDateTimeFlexible(entry) ?: continue
        if (p.timeInMillis == at.timeInMillis) {
            earliestText = entry
            break
        }
    }
    val now = System.currentTimeMillis()
    return if (at.timeInMillis < now) {
        RegistrationDeadlineInfo("Meldeschluss abgelaufen: $earliestText", "is-danger")
    } else {
        RegistrationDeadlineInfo("Meldeschluss offen: $earliestText", "is-success")
    }
}

internal fun registrationSummary(map: Map<String, ParticipationState>): Pair<Int, Int> {
    val registered = map.values.count { it.registered && !it.participated }
    val participated = map.values.count { it.participated }
    return Pair(registered, participated)
}

internal fun pendingAutoRegistrationCount(map: Map<String, ParticipationState>): Int =
    map.values.count { it.wants && !it.registered && !it.participated }

internal fun canAutoRegister(selectedId: Int?, map: Map<String, ParticipationState>, parsed: OfficialParsedDataDto?): Boolean {
    if (selectedId == null || pendingAutoRegistrationCount(map) == 0) return false
    val at = registrationDeadlineAt(parsed) ?: return true
    return at.timeInMillis >= System.currentTimeMillis()
}

internal fun workflowNotices(map: Map<String, ParticipationState>): List<WorkflowNotice> {
    val notices = mutableListOf<WorkflowNotice>()
    val pending = pendingAutoRegistrationCount(map)
    val (reg, _) = registrationSummary(map)
    if (pending > 0) {
        notices.add(WorkflowNotice("$pending zur Anmeldung bereit", "is-warning"))
    }
    if (reg > 0) {
        notices.add(WorkflowNotice("$reg Ergebnisse offen", "is-info"))
    }
    if (pending == 0 && reg == 0) {
        notices.add(WorkflowNotice("Keine offenen Aufgaben", "is-neutral"))
    }
    return notices
}

internal fun primaryHeaderAction(map: Map<String, ParticipationState>): String {
    if (pendingAutoRegistrationCount(map) > 0) return "auto-register"
    val (reg, _) = registrationSummary(map)
    if (reg > 0) return "results"
    return "members"
}

internal fun competitionStatusSummary(
    c: OfficialParsedCompetitionDto,
    parsed: OfficialParsedTournamentEnvelopeDto?,
    members: List<Member>,
    map: Map<String, ParticipationState>,
): CompetitionSummaryCounts {
    var interested = 0
    var registered = 0
    var participated = 0
    for (member in OfficialTournamentEligibility.eligibleActiveMembers(parsed, c, members)) {
        val p = getParticipation(map, c.id, member.id)
        when {
            p.participated -> participated++
            p.registered -> registered++
            p.wants -> interested++
        }
    }
    return CompetitionSummaryCounts(interested, registered, participated)
}

internal fun activeCompetitionMemberRows(
    c: OfficialParsedCompetitionDto,
    parsed: OfficialParsedTournamentEnvelopeDto?,
    members: List<Member>,
    map: Map<String, ParticipationState>,
    memberNameById: (Int) -> String,
): List<ActiveCompetitionMemberRow> {
    val rows = mutableListOf<ActiveCompetitionMemberRow>()
    for (member in OfficialTournamentEligibility.eligibleActiveMembers(parsed, c, members)) {
        val p = getParticipation(map, c.id, member.id)
        if (!p.wants && !p.registered && !p.participated) continue
        val (statusClass, statusText) = when {
            p.participated -> "status-played" to "Hat gespielt"
            p.registered -> "status-registered" to "Angemeldet"
            p.wants -> "status-wants" to "Möchte teilnehmen"
            else -> "status-none" to "Nicht interessiert"
        }
        rows.add(
            ActiveCompetitionMemberRow(
                key = participationKey(c.id, member.id),
                memberName = memberNameById(member.id),
                wants = p.wants,
                registered = p.registered,
                participated = p.participated,
                placement = p.placement,
                statusClass = statusClass,
                statusText = statusText,
            ),
        )
    }
    rows.sortWith { a, b -> collator.compare(a.memberName, b.memberName) }
    return rows
}

internal fun participantsRows(
    parsed: OfficialParsedTournamentEnvelopeDto?,
    map: Map<String, ParticipationState>,
    participantsFilter: String,
    memberNameById: (Int) -> String,
): List<ParticipantTableRow> {
    if (parsed?.parsedData == null) return emptyList()
    val comps = parsed.parsedData!!.competitions
    val compById = comps.associateBy { it.id.toString() }
    val seen = mutableSetOf<String>()
    val merged = mutableListOf<Pair<String, String>>()
    for (e in parsed.participation) {
        val cid = e.competitionId.toString()
        val mid = e.memberId.toString()
        val key = "$cid-$mid"
        if (seen.add(key)) merged.add(cid to mid)
    }
    for (k in map.keys) {
        if (seen.contains(k)) continue
        val midPart = k.substringAfterLast('-')
        val cidPart = k.substringBeforeLast('-')
        if (cidPart.isEmpty() || midPart.isEmpty()) continue
        merged.add(cidPart to midPart)
    }
    val rows = mutableListOf<ParticipantTableRow>()
    for ((competitionId, memberId) in merged) {
        val c = compById[competitionId] ?: continue
        val current = getParticipation(map, competitionId, memberId)
        val midInt = memberId.toIntOrNull() ?: continue
        val cidInt = competitionId.toIntOrNull() ?: continue
        val start = (c.startTime ?: c.startzeit ?: "–").toString()
        val base = ParticipantTableRow(
            key = "$competitionId-$memberId",
            memberId = midInt,
            memberName = memberNameById(midInt),
            competitionName = (c.ageClassCompetition ?: c.altersklasseWettbewerb ?: "").toString(),
            start = start,
            registered = current.registered,
            participated = current.participated,
            wants = current.wants,
            placement = current.placement,
        )
        when (participantsFilter) {
            "all" -> rows.add(base)
            "open" -> if (!base.registered && !base.participated) rows.add(base)
            "registered" -> if (base.registered && !base.participated) rows.add(base)
            "participated" -> if (base.participated) rows.add(base)
        }
    }
    rows.sortWith { a, b ->
        val c1 = collator.compare(a.memberName, b.memberName)
        if (c1 != 0) c1 else collator.compare(a.competitionName, b.competitionName)
    }
    return rows
}

internal fun participantsGroups(rows: List<ParticipantTableRow>): List<ParticipantGroup> {
    val byMember = LinkedHashMap<String, MutableList<ParticipantTableRow>>()
    for (row in rows) {
        byMember.getOrPut(row.memberName) { mutableListOf() }.add(row)
    }
    val groups = mutableListOf<ParticipantGroup>()
    for ((memberName, items) in byMember) {
        items.sortWith { a, b -> collator.compare(a.competitionName, b.competitionName) }
        val mid = items.firstOrNull()?.memberId ?: 0
        groups.add(ParticipantGroup(memberName = memberName, memberId = mid, items = items.toList()))
    }
    groups.sortWith { a, b -> collator.compare(a.memberName, b.memberName) }
    return groups
}

internal fun resultsRows(
    parsed: OfficialParsedTournamentEnvelopeDto?,
    map: Map<String, ParticipationState>,
    memberNameById: (Int) -> String,
): List<ParticipantTableRow> {
    if (parsed?.parsedData == null) return emptyList()
    val comps = parsed.parsedData!!.competitions
    val compById = comps.associateBy { it.id.toString() }
    val rows = mutableListOf<ParticipantTableRow>()
    for ((key, p) in map) {
        if (!p.participated) continue
        val parts = key.split("-")
        if (parts.size < 2) continue
        val competitionId = parts[0]
        val memberId = parts[1]
        val c = compById[competitionId] ?: continue
        val midInt = memberId.toIntOrNull() ?: continue
        val cidInt = competitionId.toIntOrNull() ?: continue
        rows.add(
            ParticipantTableRow(
                key = key,
                memberId = midInt,
                memberName = memberNameById(midInt),
                competitionName = (c.ageClassCompetition ?: c.altersklasseWettbewerb ?: "").toString(),
                start = (c.startTime ?: c.startzeit ?: "–").toString(),
                wants = p.wants,
                registered = p.registered,
                participated = p.participated,
                placement = p.placement,
            ),
        )
    }
    rows.sortWith { a, b ->
        val c1 = collator.compare(a.memberName, b.memberName)
        if (c1 != 0) c1 else collator.compare(a.competitionName, b.competitionName)
    }
    return rows
}

internal fun resultsGroups(rows: List<ParticipantTableRow>): List<ParticipantGroup> =
    participantsGroups(rows)

internal fun splitDateTime(str: String): Pair<String, String> {
    val s = str.trim()
    val dm = Regex("(\\d{1,2}\\.\\d{1,2}\\.\\d{4})").find(s)
    val tm = Regex("(\\d{1,2}:\\d{2})").find(s)
    return (dm?.value ?: "–") to (tm?.value ?: "–")
}

internal fun entryFeeForCompetition(parsed: OfficialParsedDataDto?, c: OfficialParsedCompetitionDto): String {
    val title = (c.ageClassCompetition ?: c.altersklasseWettbewerb ?: "").trim()
    val ageClassMatch = Regex("(U\\d+|AK\\s*\\d+)", RegexOption.IGNORE_CASE).find(title) ?: return entryFeeFromStartgeld(c)
    val ageClass = ageClassMatch.value.uppercase(Locale.ROOT).replace("\\s+".toRegex(), "")
    val feesEl = parsed?.entryFees
    if (feesEl is JsonObject) {
        val feeObj = feesEl[ageClass] as? JsonObject ?: feesEl.entries.firstOrNull { it.key.uppercase(Locale.ROOT).replace(" ", "") == ageClass }?.value as? JsonObject
        if (feeObj != null) {
            val amount = feeObj["amount"]?.toString()?.trim('"') ?: return entryFeeFromStartgeld(c)
            val currency = feeObj["currency"]?.toString()?.trim('"') ?: ""
            return "$amount$currency"
        }
    }
    return entryFeeFromStartgeld(c)
}

private fun entryFeeFromStartgeld(c: OfficialParsedCompetitionDto): String {
    val sg = c.startgeld ?: c.entryFee ?: return "–"
    val m = Regex("(\\d+(?:[,.]\\d+)?)\\s*(?:€|Euro|EUR)?").find(sg)
    return if (m != null) "${m.groupValues[1]}€" else "–"
}

internal fun competitionsForMember(
    parsed: OfficialParsedTournamentEnvelopeDto?,
    member: Member,
): List<CompetitionForMemberRow> {
    val comps = parsed?.parsedData?.competitions ?: return emptyList()
    val rows = mutableListOf<CompetitionForMemberRow>()
    comps.forEachIndexed { idx, c ->
        if (!OfficialTournamentEligibility.isEligibleForCompetition(member, c, parsed)) return@forEachIndexed
        val title = (c.ageClassCompetition ?: c.altersklasseWettbewerb ?: "").trim()
        val st = splitDateTime(c.startTime ?: c.startzeit ?: "")
        val fee = entryFeeForCompetition(parsed.parsedData, c)
        rows.add(
            CompetitionForMemberRow(
                key = idx.toString(),
                name = title,
                date = st.first,
                time = st.second,
                entryFee = fee,
                raw = c,
            ),
        )
    }
    return rows
}

internal fun computeClubHistoryRows(
    buckets: List<OfficialParticipationBucketDto>,
    range: OfficialParticipationRange,
): List<ClubHistoryRow> {
    val now = java.time.LocalDate.now(java.time.ZoneId.systemDefault())
    val (from, to) = participationRangeBoundsLocal(now, range)
    val rows = mutableListOf<ClubHistoryRow>()
    for (t in buckets) {
        for (e in t.entries) {
            if (!participationEntryInRange(e, t, from, to)) continue
            rows.add(
                ClubHistoryRow(
                    key = "club-${t.tournamentId}-${e.competitionId}-${e.memberId}",
                    memberName = e.memberName,
                    tournamentName = t.title,
                    competitionName = e.competitionName,
                    date = e.date ?: t.startDate,
                    placement = e.placement,
                ),
            )
        }
    }
    rows.sortWith { a, b ->
        val c1 = collator.compare(a.memberName ?: "", b.memberName ?: "")
        if (c1 != 0) c1 else collator.compare(a.competitionName ?: "", b.competitionName ?: "")
    }
    return rows
}

private fun participationRangeBoundsLocal(
    now: java.time.LocalDate,
    range: OfficialParticipationRange,
): Pair<java.time.LocalDate?, java.time.LocalDate?> {
    return when (range) {
        OfficialParticipationRange.ALL -> Pair(null, null)
        OfficialParticipationRange.M3 -> Pair(now.minusMonths(3), null)
        OfficialParticipationRange.M6 -> Pair(now.minusMonths(6), null)
        OfficialParticipationRange.M12 -> Pair(now.minusMonths(12), null)
        OfficialParticipationRange.Y2 -> Pair(now.minusYears(2), null)
        OfficialParticipationRange.PREV -> {
            val y = if (now.monthValue >= 7) now.year else now.year - 1
            Pair(java.time.LocalDate.of(y - 1, 7, 1), java.time.LocalDate.of(y, 6, 30))
        }
    }
}

private fun participationEntryInRange(
    e: OfficialParticipationEntryDto,
    b: OfficialParticipationBucketDto,
    from: java.time.LocalDate?,
    to: java.time.LocalDate?,
): Boolean {
    if (from == null && to == null) return true
    val d = dateFromDmy(e.date) ?: dateFromDmy(b.startDate) ?: return true
    if (from != null && d.isBefore(from)) return false
    if (to != null && d.isAfter(to)) return false
    return true
}

private fun dateFromDmy(s: String?): java.time.LocalDate? {
    if (s.isNullOrBlank()) return null
    val cal = OfficialTournamentEligibility.parseDateFlexible(s) ?: return null
    return java.time.LocalDate.of(
        cal.get(Calendar.YEAR),
        cal.get(Calendar.MONTH) + 1,
        cal.get(Calendar.DAY_OF_MONTH),
    )
}

internal fun historySummaryText(
    loading: Boolean,
    rows: List<ClubHistoryRow>,
    range: OfficialParticipationRange,
): String {
    if (loading) return "Turnierbeteiligungen werden geladen"
    if (rows.isEmpty()) return "Keine Turnierbeteiligungen im gewählten Zeitraum"
    val uniqueMembers = rows.mapNotNull { it.memberName }.distinct().size
    return "${rows.size} Einträge von $uniqueMembers Mitgliedern im Zeitraum ${range.vueKey}"
}

internal fun isAutoRegisterSuccess(response: JsonElement?): Boolean {
    if (response !is JsonObject) return false
    return response["success"]?.let { el ->
        when (el) {
            is JsonPrimitive -> el.booleanOrNull == true
            else -> false
        }
    } == true
}

internal fun autoRegisterMessage(response: JsonElement?): String? {
    if (response !is JsonObject) return null
    return response["message"]?.let { (it as? JsonPrimitive)?.contentOrNull }
}
