package com.tomhempel.rbarometer.ui

import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Rect
import androidx.annotation.ColorRes
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import androidx.appcompat.widget.AppCompatImageButton
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import com.tomhempel.rbarometer.LanguageManager
import com.tomhempel.rbarometer.MainActivity
import com.tomhempel.rbarometer.R
import com.tomhempel.rbarometer.data.PendingUploadRepository
import com.tomhempel.rbarometer.data.UnuploadedWorkRepository
import com.tomhempel.rbarometer.network.NetworkUtils
import com.tomhempel.rbarometer.network.QuestionnaireAnswerSync
import com.tomhempel.rbarometer.network.QuestionnaireCache
import com.tomhempel.rbarometer.network.SyncCoordinator
import com.tomhempel.rbarometer.network.SyncResult
import com.tomhempel.rbarometer.questionnaire.AppLanguages
import com.tomhempel.rbarometer.network.TokenStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.tomhempel.rbarometer.questionnaire.ConditionJsonParser
import com.tomhempel.rbarometer.questionnaire.GlobalValues
import com.tomhempel.rbarometer.questionnaire.QuestionItem
import com.tomhempel.rbarometer.questionnaire.QuestionnaireBase
import com.tomhempel.rbarometer.questionnaire.QuestionnaireAllInOne
import com.tomhempel.rbarometer.questionnaire.QuestionnaireGeneric
import com.tomhempel.rbarometer.questionnaire.QuestionnaireValidation
import com.tomhempel.rbarometer.session.SessionProgramHelper
import com.tomhempel.rbarometer.session.SessionCompletionResult
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.json.JSONObject
import com.tomhempel.rbarometer.utils.ScreenLoading
import com.tomhempel.rbarometer.utils.ViewUtils


class HandlerOpeningScreen(private val activity: MainActivity) {

    private val TAG = "OPENING_SCREEN"
    var languageID: String = AppLanguages.DEFAULT_ID
        private set

    private var selectedClientCode: String = ""
    private lateinit var headerWelcome: TextView
    private lateinit var coachNameDisplay: TextView
    private lateinit var labelLanguage: TextView
    private lateinit var labelClientCode: TextView
    private lateinit var clientSelectorRow: View
    private lateinit var clientCodeDisplay: TextView
    private lateinit var spinner: Spinner
    private lateinit var textView: TextView
    private lateinit var noClientsAssignedText: TextView
    private lateinit var sessionMetricClientsValue: TextView
    private lateinit var sessionMetricMeasuresValue: TextView
    private lateinit var sessionMetricSyncValue: TextView
    private lateinit var todaySessionsTitle: TextView
    private lateinit var todaySessionsSummary: TextView
    private lateinit var todaySessionsContainer: LinearLayout
    private lateinit var nextStepBannerCard: View
    private lateinit var nextStepEyebrow: TextView
    private lateinit var nextStepTitle: TextView
    private lateinit var nextStepBody: TextView
    private lateinit var nextStepActionButton: MaterialButton
    private lateinit var questionnaireListHost: View
    private lateinit var questionnaireScroll: View
    private lateinit var buttonContainer: LinearLayout
    private lateinit var bottomUploadDock: View
    private lateinit var reviewScoresButton: MaterialButton
    private lateinit var uploadButton: MaterialButton
    private lateinit var questionnaireListLoading: View
    private lateinit var statusChipContainer: LinearLayout
    private lateinit var statusDot: ImageView
    private lateinit var statusOnline: TextView
    private lateinit var refreshButton: AppCompatImageButton
    private lateinit var settingsButton: AppCompatImageButton
    private lateinit var refreshProgress: ProgressBar

    private var loadHandler: LoadButtonHandler? = null
    private var refreshJob: Job? = null
    private var nextStepJob: Job? = null
    private var sessionViewsJob: Job? = null
    private var spinnerReady = false
    private var suppressLanguageCallback = false
    private var firstClientLoadDone = false
    private var pendingUploadDialogShowing = false
    private val afterFirstClientLoadCallbacks = mutableListOf<() -> Unit>()

    private val dynamicButtons = mutableListOf<Button>()
    private val questionnaireFiles = mutableMapOf<Button, String>()

    private val cardParts = mutableMapOf<Button, QuestionnaireCardUi.CardParts>()

    private val completedQuestionnaireIds: MutableSet<String> = mutableSetOf()
    private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList()
    private var pendingUploadQuestionnaireIds: Set<String> = emptySet()

    private var uiFreeze: Boolean = false

    private enum class NextStepAction {
        NONE,
        QUESTIONNAIRE,
        REVIEW,
        UPLOAD,
    }

    private data class ClientSessionCard(
        val view: SessionProgramHelper.ClientSessionView,
    )

    private fun color(@ColorRes res: Int) = ContextCompat.getColor(activity, res)

    private fun t(id: String) = LanguageManager.getText(languageID, id)

    private fun tOr(id: String, fallback: String): String {
        val raw = t(id)
        return if (raw == id) fallback else raw
    }

    private fun hubFallback(de: String, en: String): String =
        if (languageID == "GERMAN") de else en

    private fun groupHeaderStringKey(categoryKey: String): String = "questionnaire_group_$categoryKey"

    private fun isPendingUpload(fileName: String): Boolean =
        pendingUploadQuestionnaireIds.any { fileName.contains(it, ignoreCase = true) }

    private fun chipTextKey(completed: Boolean, enabled: Boolean, fileName: String): String = when {
        completed && isPendingUpload(fileName) -> "questionnaire_chip_edit_pending"
        completed -> "questionnaire_chip_edit"
        enabled -> "start"
        else -> "locked"
    }

    private fun lockHintForEntry(entry: QuestionItem.QuestionnaireEntry?): String? {
        val key = entry?.messageKey?.takeIf { it.isNotBlank() } ?: return null
        val text = t(key).trim()
        if (text.isBlank() || text.equals(key, ignoreCase = true)) {
            Log.w(TAG, "lock hint missing translation for key=$key")
            return null
        }
        return text
    }

    private fun assignedClientsForUi(): List<String> =
        QuestionnaireCache.getAssignedClients(activity)
            .filter { !it.equals(DevSettingsAccess.CLIENT_CODE, ignoreCase = true) }

    private fun isClientAssigned(clientCode: String?): Boolean {
        if (clientCode.isNullOrBlank()) return false
        return assignedClientsForUi().any { it.equals(clientCode, ignoreCase = true) }
    }

    /** Clears in-memory load state when the active client is missing or no longer assigned. */
    private fun clearLoadedClientState() {
        GlobalValues.LAST_CLIENT_CODE = null
        GlobalValues.LOADED_CLIENT_CODE = null
        completedQuestionnaireIds.clear()
        if (dynamicButtons.isNotEmpty()) {
            applySetButtonsEnabled(emptyList(), allowCompleted = false, force = true)
            applyUpdateButtonTexts(force = true)
        }
    }

    fun runAfterFirstClientLoad(block: () -> Unit) {
        if (firstClientLoadDone) block() else afterFirstClientLoadCallbacks.add(block)
    }

    /** Updates client picker after a background assigned-clients sync (single UI reload). */
    fun onAssignedClientsSynced() {
        refreshClientSpinner()
        renderTodaySessions()
        if (selectedClientCode.isNotBlank() && isClientAssigned(selectedClientCode)) {
            refreshLoadedClientUi()
        }
    }

    fun init() {
        refreshJob?.cancel()
        nextStepJob?.cancel()
        sessionViewsJob?.cancel()
        activity.bumpAssignedClientsRefreshGeneration()
        firstClientLoadDone = false
        afterFirstClientLoadCallbacks.clear()
        activity.setContentView(R.layout.opening_screen)
        SyncCoordinator.loadCachedTranslations(activity)
        languageID = AppLanguageStore.get(activity)

        bindViews()
        loadQuestionnaireOrder()
        renderTodaySessions()
        if (assignedClientsForUi().isEmpty()) {
            createQuestionnaireButtons()
        } else {
            buttonContainer.removeAllViews()
            dynamicButtons.clear()
            questionnaireFiles.clear()
            cardParts.clear()
        }
        setupCoachDisplay()
        setupLanguageSpinner()
        setupLoadHandler()
        setupClientSelector()
        setupReviewScoresButton()
        setupUploadButton()
        setupRefreshButton()
        setupSettingsButton()

        updateStatusStrip()
        updateQuestionnaireSectionVisibility()
        refreshPendingUploadBadge()
        if (selectedClientCode.isNotBlank() &&
            selectedClientCode.equals(GlobalValues.LOADED_CLIENT_CODE, ignoreCase = true)
        ) {
            loadHandler?.triggerLoad()
        }
    }

    /** Lightweight refresh when returning from a questionnaire or upload — avoids full layout rebuild. */
    fun refreshOpeningUi() {
        if (!isOpeningScreenVisible()) {
            init()
            return
        }
        updateStatusStrip()
        updateQuestionnaireSectionVisibility()
        renderTodaySessions()
        refreshPendingUploadBadge()
        updateSessionMetrics()
        if (assignedClientsForUi().isEmpty()) {
            applyUpdateButtonTexts(force = true)
        }
    }

    private fun isOpeningScreenVisible(): Boolean =
        activity.findViewById<View?>(R.id.todaySessionsContainer) != null

    private fun bindViews() {
        headerWelcome = activity.findViewById(R.id.headerWelcome)
        coachNameDisplay = activity.findViewById(R.id.coachNameDisplay)
        labelLanguage = activity.findViewById(R.id.labelLanguage)
        labelClientCode = activity.findViewById(R.id.labelClientCode)
        clientSelectorRow = activity.findViewById(R.id.clientSelectorRow)
        clientCodeDisplay = activity.findViewById(R.id.clientCodeDisplay)
        spinner = activity.findViewById(R.id.string_spinner1)
        textView = activity.findViewById(R.id.textView)
        noClientsAssignedText = activity.findViewById(R.id.noClientsAssignedText)
        sessionMetricClientsValue = activity.findViewById(R.id.sessionMetricClientsValue)
        sessionMetricMeasuresValue = activity.findViewById(R.id.sessionMetricMeasuresValue)
        sessionMetricSyncValue = activity.findViewById(R.id.sessionMetricSyncValue)
        todaySessionsTitle = activity.findViewById(R.id.todaySessionsTitle)
        todaySessionsSummary = activity.findViewById(R.id.todaySessionsSummary)
        todaySessionsContainer = activity.findViewById(R.id.todaySessionsContainer)
        nextStepBannerCard = activity.findViewById(R.id.nextStepBannerCard)
        nextStepEyebrow = activity.findViewById(R.id.nextStepEyebrow)
        nextStepTitle = activity.findViewById(R.id.nextStepTitle)
        nextStepBody = activity.findViewById(R.id.nextStepBody)
        nextStepActionButton = activity.findViewById(R.id.nextStepActionButton)
        questionnaireListHost = activity.findViewById(R.id.questionnaireListHost)
        questionnaireScroll = activity.findViewById(R.id.questionnaireScroll)
        bottomUploadDock = activity.findViewById(R.id.bottomUploadDock)
        questionnaireListLoading = activity.findViewById(R.id.questionnaireListLoading)
        buttonContainer = activity.findViewById(R.id.buttonContainer)
        reviewScoresButton = activity.findViewById(R.id.reviewScoresButton)
        uploadButton = activity.findViewById(R.id.uploadButton)

        statusChipContainer = activity.findViewById(R.id.statusChipContainer)
        statusDot = activity.findViewById(R.id.statusDot)
        statusOnline = activity.findViewById(R.id.statusOnline)
        refreshButton = activity.findViewById(R.id.refreshButton)
        settingsButton = activity.findViewById(R.id.settingsButton)
        refreshProgress = activity.findViewById(R.id.refreshProgress)
        refreshButton.contentDescription = t("refresh")
        settingsButton.contentDescription = tOr("settings", hubFallback("Einstellungen", "Settings"))
        textView.text = hubFallback("Sitzungsinstrumente", "Session measures")
        applyHubLabels()
        updateSessionMetrics()
        renderTodaySessions()
        applyCoachDisplay()
    }

    private fun applyHubLabels() {
        if (!::labelLanguage.isInitialized) return
        headerWelcome.text = "Barometer"
        labelLanguage.text = tOr("language", hubFallback("Sprache", "Language"))
        labelClientCode.text = hubFallback("Aktiver Klient", "Active client")
    }

    private fun applyCoachDisplay() {
        if (!::coachNameDisplay.isInitialized) return
        val username = TokenStore.getUsername(activity)?.trim().orEmpty()
        coachNameDisplay.text = if (username.isBlank()) {
            hubFallback("Sitzungskonsole", "Session console")
        } else {
            hubFallback("Beraterkonsole: $username", "Counselor console: $username")
        }
    }

    private fun updateSessionMetrics() {
        if (!::sessionMetricClientsValue.isInitialized) return
        sessionMetricClientsValue.text = assignedClientsForUi().size.toString()
        sessionMetricMeasuresValue.text = questionnaireEntries.size.toString()
        CoroutineScope(Dispatchers.Main).launch {
            val count = withContext(Dispatchers.IO) {
                PendingUploadRepository.countUploadReady(activity)
            }
            if (::sessionMetricSyncValue.isInitialized) {
                sessionMetricSyncValue.text = if (count > 0) count.toString() else "OK"
            }
        }
    }

    private fun renderTodaySessions() {
        if (!::todaySessionsContainer.isInitialized) return
        todaySessionsTitle.text = hubFallback("Sitzungen", "Sessions")
        sessionViewsJob?.cancel()
        sessionViewsJob = CoroutineScope(Dispatchers.Main).launch {
            val views = try {
                withContext(Dispatchers.IO) {
                    SessionProgramHelper.buildClientSessionViews(activity)
                }
            } catch (e: Exception) {
                Log.e(TAG, "renderTodaySessions failed", e)
                emptyList()
            }
            if (!isActive) return@launch
            bindTodaySessions(views)
        }
    }

    private fun bindTodaySessions(views: List<SessionProgramHelper.ClientSessionView>) {
        val ready = views.filter {
            SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.READY
        }
        val inProgress = views.filter {
            SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.IN_PROGRESS
        }
        val waitingUpload = views.filter {
            SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.WAITING_UPLOAD
        }
        val complete = views.filter {
            SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.COMPLETE
        }
        val dueCount = ready.size
        todaySessionsSummary.text = if (dueCount == 1) {
            hubFallback("1 Sitzung bereit", "1 session ready")
        } else {
            hubFallback("$dueCount Sitzungen bereit", "$dueCount sessions ready")
        }
        todaySessionsContainer.removeAllViews()
        addSessionGroup(
            title = hubFallback("Bereit", "Ready"),
            sessions = ready.map { ClientSessionCard(it) },
        )
        addSessionGroup(
            title = hubFallback("In Bearbeitung", "In progress"),
            sessions = inProgress.map { ClientSessionCard(it) },
        )
        addSessionGroup(
            title = hubFallback("Warte auf Upload", "Waiting for upload"),
            sessions = waitingUpload.map { ClientSessionCard(it) },
        )
        addSessionGroup(
            title = hubFallback("Abgeschlossen", "Complete"),
            sessions = complete.map { ClientSessionCard(it) },
        )
        if (views.isEmpty()) {
            todaySessionsContainer.addView(TextView(activity).apply {
                text = hubFallback(
                    "Keine Sitzungsdaten — bitte synchronisieren.",
                    "No session data — please sync.",
                )
                setPadding(dp(2), dp(8), 0, dp(4))
                setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
                setTextColor(color(R.color.brand_text_muted))
            })
        }
    }

    private fun addSessionGroup(title: String, sessions: List<ClientSessionCard>) {
        if (sessions.isEmpty()) return
        todaySessionsContainer.addView(TextView(activity).apply {
            text = title
            setPadding(dp(2), if (todaySessionsContainer.childCount == 0) 0 else dp(14), 0, dp(4))
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
            setTypeface(typeface, android.graphics.Typeface.BOLD)
            setTextColor(color(R.color.brand_text_dark))
        })
        sessions.forEach { session ->
            todaySessionsContainer.addView(createTodaySessionCard(session))
        }
    }

    private fun createTodaySessionCard(session: ClientSessionCard): View {
        val view = session.view
        val progress = view.progress
        val card = MaterialCardView(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            ).also {
                it.setMargins(0, 0, 0, dp(10))
            }
            radius = dp(14).toFloat()
            strokeWidth = dp(if (view.canEnterSession) 2 else 1)
            setStrokeColor(color(if (view.canEnterSession) R.color.brand_purple else R.color.brand_header_stroke))
            setCardBackgroundColor(color(R.color.brand_header_surface))
            cardElevation = dp(2).toFloat()
        }
        val content = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(16), dp(14), dp(16), dp(14))
        }
        val header = LinearLayout(activity).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
        }
        val titleBlock = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
        }
        titleBlock.addView(TextView(activity).apply {
            text = view.clientCode
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
            setTypeface(typeface, android.graphics.Typeface.BOLD)
            setTextColor(color(R.color.brand_text_dark))
        })
        titleBlock.addView(TextView(activity).apply {
            text = hubFallback(
                "Zyklus ${progress.cycleNumber} · Sitzung ${progress.currentSessionNumber ?: "—"} von ${progress.totalSessions}",
                "Cycle ${progress.cycleNumber} · Session ${progress.currentSessionNumber ?: "—"} of ${progress.totalSessions}",
            )
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
            setTextColor(color(R.color.brand_text_muted))
        })
        header.addView(titleBlock)
        header.addView(sessionStatusChip(view))
        content.addView(header)

        progress.currentSessionName?.takeIf { it.isNotBlank() }?.let { sessionName ->
            content.addView(TextView(activity).apply {
                text = sessionName
                setPadding(0, dp(10), 0, dp(8))
                setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
                setTypeface(typeface, android.graphics.Typeface.BOLD)
                setTextColor(color(R.color.brand_text_dark))
            })
        }
        content.addView(TextView(activity).apply {
            text = hubFallback("Messpunkte in dieser Sitzung", "Measures in this session")
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
            setTypeface(typeface, android.graphics.Typeface.BOLD)
            setTextColor(color(R.color.brand_text_muted))
        })
        view.measures.forEach { measure ->
            content.addView(
                SessionMeasureUi.measureRow(
                    context = activity,
                    label = measure.displayName,
                    checked = measure.completedInCycle,
                ),
            )
        }

        if (view.canEnterSession) {
            val canResume = view.canResumeSession
            val editBeforeUpload = view.isSessionMeasuresComplete && view.hasPendingUpload
            val actions = LinearLayout(activity).apply {
                orientation = LinearLayout.HORIZONTAL
                setPadding(0, dp(12), 0, 0)
            }
            actions.addView(
                todayAction(
                    hubFallback(
                        when {
                            editBeforeUpload -> "Sitzung bearbeiten"
                            canResume -> "Sitzung fortsetzen"
                            else -> "Sitzung starten"
                        },
                        when {
                            editBeforeUpload -> "Edit session"
                            canResume -> "Continue session"
                            else -> "Start session"
                        },
                    ),
                    filled = true,
                    weight = 1f,
                ) {
                    startSessionForClient(view.clientCode)
                },
            )
            content.addView(actions)
        }
        card.addView(content)
        return card
    }

    private fun sessionStatusChip(view: SessionProgramHelper.ClientSessionView): TextView {
        val canResume = view.canResumeSession
        val (label, style) = when (SessionProgramHelper.sessionDisplayBucket(view)) {
            SessionProgramHelper.SessionDisplayBucket.COMPLETE ->
                hubFallback("Abgeschlossen", "Complete") to "complete"
            SessionProgramHelper.SessionDisplayBucket.WAITING_UPLOAD ->
                hubFallback("Warte auf Upload", "Waiting for upload") to "upload"
            SessionProgramHelper.SessionDisplayBucket.IN_PROGRESS ->
                hubFallback(
                    if (canResume) "Pausiert" else "In Bearbeitung",
                    if (canResume) "Paused" else "In progress",
                ) to "waiting"
            SessionProgramHelper.SessionDisplayBucket.READY ->
                hubFallback("Bereit", "Ready") to "ready"
        }
        return TextView(activity).apply {
            text = label
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
            setTypeface(typeface, android.graphics.Typeface.BOLD)
            setTextColor(
                color(
                    when (style) {
                        "waiting" -> R.color.brand_chip_locked_text
                        "upload" -> R.color.brand_text_on_dark
                        else -> R.color.brand_text_on_dark
                    },
                ),
            )
            setBackgroundResource(
                when (style) {
                    "complete" -> R.drawable.bg_chip_locked
                    "waiting" -> R.drawable.bg_chip_locked
                    "upload" -> R.drawable.bg_chip_amber
                    else -> R.drawable.bg_chip_green
                },
            )
            setPadding(dp(10), dp(6), dp(10), dp(6))
        }
    }

    private fun startSessionForClient(clientCode: String) {
        if (!isClientAssigned(clientCode)) {
            Toast.makeText(activity, t("no_profile"), Toast.LENGTH_LONG).show()
            return
        }
        selectClient(clientCode, userInitiated = false)
        GlobalValues.LOADED_CLIENT_CODE = clientCode
        activity.startSessionFlow(clientCode, languageID)
    }

    fun showSessionCompletionDialog(result: SessionCompletionResult) {
        selectClient(result.clientCode, userInitiated = false)
        GlobalValues.LAST_CLIENT_CODE = result.clientCode
        GlobalValues.LOADED_CLIENT_CODE = result.clientCode

        val title = hubFallback("Sitzung abgeschlossen", "Session complete")
        val message = when {
            result.sessionName.isNotBlank() && result.canUpload ->
                hubFallback(
                    "${result.sessionName} ist abgeschlossen. Sie können die Daten jetzt hochladen oder vor dem Upload noch bearbeiten.",
                    "${result.sessionName} is complete. You can upload now or edit before uploading.",
                )
            result.sessionName.isNotBlank() ->
                hubFallback(
                    "${result.sessionName} ist abgeschlossen.",
                    "${result.sessionName} is complete.",
                )
            result.canUpload ->
                hubFallback(
                    "Alle Messpunkte dieser Sitzung sind erledigt. Sie können die Daten jetzt hochladen oder vor dem Upload noch bearbeiten.",
                    "All measures in this session are complete. You can upload now or edit before uploading.",
                )
            else ->
                hubFallback(
                    "Alle Messpunkte dieser Sitzung sind erledigt.",
                    "All measures in this session are complete.",
                )
        }

        LogoutDialogs.showThreeAction(
            activity = activity,
            title = title,
            message = message,
            tone = LogoutDialogs.Tone.Info,
            primaryLabel = hubFallback("Hochladen", "Upload"),
            secondaryLabel = hubFallback("Bearbeiten", "Edit"),
            tertiaryLabel = hubFallback("Schließen", "Close"),
            showPrimary = result.canUpload,
            onPrimary = { openUploadOverview(syncAfterClose = true) },
            onSecondary = { startSessionForClient(result.clientCode) },
        )
    }

    private fun todayAction(
        label: String,
        filled: Boolean,
        weight: Float,
        fixedWidth: Int? = null,
        onClick: (() -> Unit)? = null,
    ): MaterialButton =
        MaterialButton(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                fixedWidth ?: 0,
                dp(40),
                if (fixedWidth == null) weight else 0f,
            ).also { it.setMargins(0, 0, dp(8), 0) }
            text = label
            isAllCaps = false
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
            cornerRadius = dp(18)
            strokeWidth = dp(1)
            insetTop = 0
            insetBottom = 0
            setTextColor(color(if (filled) R.color.brand_text_on_dark else R.color.brand_purple))
            backgroundTintList = ColorStateList.valueOf(color(if (filled) R.color.brand_purple else R.color.brand_header_surface))
            strokeColor = ColorStateList.valueOf(color(R.color.brand_purple))
            setOnClickListener { onClick?.invoke() }
        }

    private fun loadQuestionnaireOrder() {
        val serverItems = QuestionnaireCache.getQuestionnaireList(activity)
        Log.d(TAG, "loadQuestionnaireOrder: serverItems from cache = ${serverItems.map { it.id }}")

        if (serverItems.isNotEmpty()) {
            questionnaireEntries = serverItems.map { item ->
                QuestionItem.QuestionnaireEntry(
                    file = item.id,
                    condition = ConditionJsonParser.parse(item.conditionJson),
                    showPoints = item.showPoints,
                    serverName = item.name,
                    categoryKey = item.categoryKey,
                    messageKey = ConditionJsonParser.messageKey(item.conditionJson),
                )
            }
            Log.d(TAG, "loadQuestionnaireOrder: from synced cache => ${questionnaireEntries.map { it.file }}")
            return
        }

        Log.w(TAG, "loadQuestionnaireOrder: no synced cache — sync required before questionnaires appear")
        questionnaireEntries = emptyList()
        Log.d(TAG, "loadQuestionnaireOrder: final questionnaireEntries count = ${questionnaireEntries.size}")
    }

    private data class QuestionnaireListBlock(
        val categoryKey: String?,
        val entries: List<QuestionItem.QuestionnaireEntry>,
    ) {
        val isCategorized: Boolean get() = !categoryKey.isNullOrBlank()
    }

    /** Categorized questionnaires with the same key are grouped; each uncategorized entry is its own card. */
    private fun buildQuestionnaireListBlocks(
        entries: List<QuestionItem.QuestionnaireEntry>,
    ): List<QuestionnaireListBlock> {
        if (entries.isEmpty()) return emptyList()
        val blocks = mutableListOf<QuestionnaireListBlock>()
        var currentKey: String? = null
        var bucket = mutableListOf<QuestionItem.QuestionnaireEntry>()

        fun flush() {
            if (bucket.isEmpty()) return
            blocks.add(QuestionnaireListBlock(currentKey, bucket.toList()))
            bucket = mutableListOf()
        }

        for (entry in entries) {
            val cat = entry.categoryKey?.trim()?.takeIf { it.isNotEmpty() }
            when {
                cat == null -> {
                    flush()
                    currentKey = null
                    blocks.add(QuestionnaireListBlock(null, listOf(entry)))
                }
                cat != currentKey -> {
                    flush()
                    currentKey = cat
                    bucket.add(entry)
                }
                else -> bucket.add(entry)
            }
        }
        flush()
        return blocks
    }

    /** Groups questionnaire rows in a card; optional [categoryKey] shows a section title. */
    private fun createQuestionnaireGroupCard(categoryKey: String?): LinearLayout {
        val card = MaterialCardView(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            ).also { it.setMargins(0, dp(12), 0, 0) }
            radius = dp(28).toFloat()
            strokeWidth = dp(1)
            setStrokeColor(ColorStateList.valueOf(color(R.color.brand_header_stroke)))
            setCardBackgroundColor(color(R.color.brand_header_surface))
            cardElevation = dp(1).toFloat()
            useCompatPadding = true
        }
        val content = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            )
            setPadding(dp(18), dp(18), dp(18), dp(14))
        }
        val inner = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            )
        }
        if (!categoryKey.isNullOrBlank()) {
            val title = TextView(activity).apply {
                text = t(groupHeaderStringKey(categoryKey))
                setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
                setTextColor(color(R.color.brand_text_dark))
                setTypeface(typeface, android.graphics.Typeface.BOLD)
                letterSpacing = 0.03f
                setPadding(dp(4), 0, dp(4), dp(12))
            }
            content.addView(title)
        }
        content.addView(inner)
        card.addView(content)
        buttonContainer.addView(card)
        return inner
    }

    private fun addQuestionnaireRow(
        parent: LinearLayout,
        entry: QuestionItem.QuestionnaireEntry,
        vMargin: Int,
        startEnabled: MutableList<Button>,
    ): MaterialButton {
        val row = ConstraintLayout(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            ).also { it.setMargins(0, vMargin, 0, vMargin) }
            minimumHeight = dp(96)
        }
        val btn = MaterialButton(activity).apply {
            layoutParams = ConstraintLayout.LayoutParams(0, 0).apply {
                topToTop = ConstraintLayout.LayoutParams.PARENT_ID
                bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                startToStart = ConstraintLayout.LayoutParams.PARENT_ID
                endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
            }
            text = ""
            isAllCaps = false
            setMinimumWidth(0)
            setMinimumHeight(dp(92))
            cornerRadius = dp(28)
            strokeWidth = dp(1)
            strokeColor = ColorStateList.valueOf(color(R.color.brand_control_chip_stroke))
            backgroundTintList = ColorStateList.valueOf(color(R.color.brand_control_chip_bg))
            rippleColor = ColorStateList.valueOf(color(R.color.brand_ripple_on_surface))
            gravity = Gravity.START or Gravity.CENTER_VERTICAL
            insetTop = 0
            insetBottom = 0
            id = View.generateViewId()
        }
        val chip = TextView(activity).apply {
            id = View.generateViewId()
            setPadding(dp(14), dp(9), dp(14), dp(9))
            setTextColor(color(R.color.brand_text_on_dark))
            text = t("start")
            setBackgroundResource(R.drawable.bg_chip_amber)
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
            QuestionnaireCardUi.styleStatusChip(activity, this)
            layoutParams = ConstraintLayout.LayoutParams(
                ConstraintLayout.LayoutParams.WRAP_CONTENT,
                ConstraintLayout.LayoutParams.WRAP_CONTENT,
            ).apply {
                topToTop = ConstraintLayout.LayoutParams.PARENT_ID
                bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
                marginEnd = dp(16)
                constrainedWidth = true
                matchConstraintMaxWidth = maxWidth
            }
        }
        val textColumn = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            layoutParams = ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT).apply {
                topToTop = ConstraintLayout.LayoutParams.PARENT_ID
                bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                startToStart = ConstraintLayout.LayoutParams.PARENT_ID
                endToStart = chip.id
                marginStart = dp(22)
                marginEnd = dp(14)
                topMargin = dp(18)
                bottomMargin = dp(18)
                constrainedWidth = true
                horizontalBias = 0f
            }
        }
        val textLp = LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT,
        )
        val tvTitle = TextView(activity).apply {
            layoutParams = textLp
            setPadding(0, 0, 0, dp(2))
            QuestionnaireCardUi.styleTitle(activity, this)
        }
        val tvSubtitle = TextView(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            )
            visibility = View.GONE
            QuestionnaireCardUi.styleSubtitle(activity, this)
        }
        textColumn.addView(tvTitle)
        textColumn.addView(tvSubtitle)
        val openQuestionnaire = View.OnClickListener { onQuestionnaireCardClicked(btn) }
        btn.setOnClickListener(openQuestionnaire)
        row.setOnClickListener(openQuestionnaire)
        textColumn.setOnClickListener(openQuestionnaire)
        chip.setOnClickListener(openQuestionnaire)
        row.addView(btn)
        row.addView(textColumn)
        row.addView(chip)
        parent.addView(row)
        dynamicButtons.add(btn)
        questionnaireFiles[btn] = entry.file
        cardParts[btn] = QuestionnaireCardUi.CardParts(tvTitle, tvSubtitle, chip)

        val key = entry.file.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
        tvTitle.text = entry.serverName?.takeIf { it.isNotBlank() } ?: t(key)

        if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
        return btn
    }

    private fun createQuestionnaireButtons() {
        buttonContainer.removeAllViews()
        dynamicButtons.clear()
        questionnaireFiles.clear()
        cardParts.clear()
        val rowMargin = dp(8)
        val startEnabled = mutableListOf<Button>()

        for (block in buildQuestionnaireListBlocks(questionnaireEntries)) {
            val groupContainer = createQuestionnaireGroupCard(block.categoryKey)
            block.entries.forEachIndexed { index, entry ->
                val top = if (index == 0) dp(4) else rowMargin
                addQuestionnaireRow(groupContainer, entry, top, startEnabled)
            }
        }

        applyUpdateButtonTexts(force = false)
        applySetButtonsEnabled(startEnabled, allowCompleted = false, force = false)
        updateNextStepBanner()
    }

    private fun setupRefreshButton() {
        refreshButton.setOnClickListener { performManualRefresh() }
    }

    private fun setupSettingsButton() {
        settingsButton.setOnClickListener { showSettingsSheet() }
    }

    private fun showSettingsSheet() {
        val dialog = BottomSheetDialog(activity)
        val container = LinearLayout(activity).apply {
            orientation = LinearLayout.VERTICAL
            setBackgroundColor(color(R.color.brand_surface))
            setPadding(dp(20), dp(12), dp(20), dp(28))
        }

        val handle = View(activity).apply {
            layoutParams = LinearLayout.LayoutParams(dp(44), dp(5)).also {
                it.gravity = Gravity.CENTER_HORIZONTAL
                it.bottomMargin = dp(18)
            }
            background = android.graphics.drawable.GradientDrawable().apply {
                shape = android.graphics.drawable.GradientDrawable.RECTANGLE
                cornerRadius = dp(100).toFloat()
                setColor(color(R.color.brand_bottom_sheet_handle))
            }
        }

        val titleView = TextView(activity).apply {
            text = tOr("settings", hubFallback("Einstellungen", "Settings"))
            setTextColor(color(R.color.brand_text_dark))
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
            setTypeface(typeface, android.graphics.Typeface.BOLD)
        }

        val dashboardButton = MaterialButton(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            ).also {
                it.bottomMargin = dp(10)
            }
            text = hubFallback("Dashboard", "Dashboard")
            isAllCaps = false
            icon = ContextCompat.getDrawable(activity, R.drawable.ic_person_24)
            iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
            iconPadding = dp(10)
            iconTint = ColorStateList.valueOf(color(R.color.brand_purple))
            setTextColor(color(R.color.brand_text_dark))
            textAlignment = View.TEXT_ALIGNMENT_VIEW_START
            gravity = Gravity.START or Gravity.CENTER_VERTICAL
            backgroundTintList = ColorStateList.valueOf(color(R.color.brand_header_surface))
            strokeColor = ColorStateList.valueOf(color(R.color.brand_header_stroke))
            strokeWidth = dp(1)
            cornerRadius = dp(20)
            insetTop = 0
            insetBottom = 0
            setPadding(dp(12), dp(14), dp(12), dp(14))
            setOnClickListener {
                dialog.dismiss()
                DashboardHandler(activity) {
                    activity.setBackPressInterceptor(null)
                    init()
                }.show()
            }
        }

        val logoutButton = MaterialButton(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
            )
            text = tOr("logout", hubFallback("Abmelden", "Log out"))
            isAllCaps = false
            icon = ContextCompat.getDrawable(activity, R.drawable.ic_lock_24)
            iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
            iconPadding = dp(10)
            iconTint = ColorStateList.valueOf(color(R.color.brand_destructive_text))
            setTextColor(color(R.color.brand_destructive_text))
            textAlignment = View.TEXT_ALIGNMENT_VIEW_START
            gravity = Gravity.START or Gravity.CENTER_VERTICAL
            backgroundTintList = ColorStateList.valueOf(color(R.color.brand_destructive_fill))
            strokeColor = ColorStateList.valueOf(color(R.color.brand_destructive_fill))
            strokeWidth = dp(1)
            cornerRadius = dp(20)
            insetTop = 0
            insetBottom = 0
            setPadding(dp(12), dp(14), dp(12), dp(14))
            setOnClickListener {
                dialog.dismiss()
                activity.attemptLogout(languageID)
            }
        }

        container.addView(handle)
        container.addView(titleView)
        container.addView(View(activity).apply {
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                dp(14),
            )
        })
        container.addView(dashboardButton)
        container.addView(logoutButton)
        dialog.setContentView(container)
        dialog.show()
    }

    private fun performManualRefresh() {
        val token = TokenStore.getToken(activity)
        if (token.isNullOrBlank()) {
            activity.requestLogin()
            return
        }
        if (!NetworkUtils.isOnline(activity)) {
            Toast.makeText(activity, t("offline"), Toast.LENGTH_LONG).show()
            return
        }

        performSyncWithOverlay(
            token = token,
            showSuccessToast = true,
            onFinished = {},
        )
    }

    private fun performSyncWithOverlay(
        token: String,
        showSuccessToast: Boolean,
        onFinished: () -> Unit,
    ) {
        refreshJob?.cancel()
        setRefreshInProgress(true)
        activity.showSyncProgressOverlay(languageID)
        refreshJob = CoroutineScope(Dispatchers.Main).launch {
            val syncResult = withContext(Dispatchers.IO) {
                SyncCoordinator.refreshManual(
                    context = activity,
                    token = token,
                    onProgress = { activity.reportSyncProgress(it) },
                )
            }
            when (syncResult) {
                SyncResult.AuthRequired -> {
                    setRefreshInProgress(false)
                    activity.hideSyncProgressOverlay()
                    onFinished()
                    activity.requestLogin()
                    return@launch
                }
                SyncResult.Offline -> {
                    setRefreshInProgress(false)
                    activity.hideSyncProgressOverlay()
                    SyncCoordinator.toastForResult(activity, languageID, syncResult)
                    onFinished()
                    return@launch
                }
                else -> {
                    SyncCoordinator.loadCachedTranslations(activity)
                    QuestionnaireAnswerSync.invalidatePullCache(activity)
                    reloadContentAfterSync()
                    setRefreshInProgress(false)
                    activity.hideSyncProgressOverlay()
                    if (syncResult is SyncResult.Failure) {
                        Log.e(TAG, "manual refresh failed")
                        SyncCoordinator.toastForResult(activity, languageID, syncResult)
                    } else if (showSuccessToast) {
                        activity.markSuccessfulSync()
                        Toast.makeText(
                            activity,
                            tOr("refresh_complete", hubFallback("Aktualisierung abgeschlossen", "Refresh complete")),
                            Toast.LENGTH_SHORT,
                        ).show()
                    } else if (syncResult is SyncResult.Success) {
                        activity.markSuccessfulSync()
                    }
                    onFinished()
                }
            }
        }
    }

    fun showStartupUploadPrompt() = showPendingUploadPrompt()

    private fun showPendingUploadPrompt() {
        if (pendingUploadDialogShowing) {
            return
        }
        pendingUploadDialogShowing = true
        CoroutineScope(Dispatchers.Main).launch {
            val uploadReady = withContext(Dispatchers.IO) {
                PendingUploadRepository.hasUploadReady(activity)
            }
            if (!uploadReady) {
                pendingUploadDialogShowing = false
                return@launch
            }
            val summary = withContext(Dispatchers.IO) { UnuploadedWorkRepository.summarize() }
            val details = SessionLogout.formatPendingDetails(languageID, summary)
            LogoutDialogs.showAction(
                activity = activity,
                languageId = languageID,
                title = tOr("upload_pending_title", hubFallback("Upload bereit", "Upload ready")),
                message = tOr(
                    "upload_pending_review_message",
                    hubFallback(
                        "Bitte prüfen und laden Sie die fertigen Daten hoch, sobald sie bereit sind.",
                        "Please review and upload the completed data when it is ready.",
                    ),
                ),
                details = details,
                tone = LogoutDialogs.Tone.Warning,
                primaryLabel = tOr("upload", "Upload"),
                secondaryLabel = tOr("later", hubFallback("Später", "Later")),
                onPrimary = {
                    pendingUploadDialogShowing = false
                    openUploadOverview(syncAfterClose = true)
                },
            )
            activity.window.decorView.postDelayed({
                pendingUploadDialogShowing = false
            }, 300L)
        }
    }

    private fun setRefreshInProgress(inProgress: Boolean) {
        refreshButton.isEnabled = !inProgress
        refreshButton.alpha = if (inProgress) 0.4f else 1f
        refreshProgress.visibility = if (inProgress) View.VISIBLE else View.GONE
        refreshButton.setImageDrawable(
            if (inProgress) null else ContextCompat.getDrawable(activity, R.drawable.ic_refresh_24)
        )
        uiFreeze = inProgress
    }

    private fun reloadContentAfterSync() {
        loadQuestionnaireOrder()
        createQuestionnaireButtons()
        refreshClientSpinner()
        applyUpdateButtonTexts(force = true)
        updateStatusStrip()
        updateSessionMetrics()
        renderTodaySessions()
        updateQuestionnaireSectionVisibility()
        refreshLoadedClientUi()
    }

    /** Re-read Room completion/answer state for the spinner selection (no server pull). */
    fun refreshLoadedClientUi() {
        loadHandler?.triggerLoad()
    }

    private fun updateQuestionnaireSectionVisibility() {
        val hasClients = assignedClientsForUi().isNotEmpty()
        if (hasClients) {
            noClientsAssignedText.visibility = View.GONE
            textView.visibility = View.GONE
            nextStepBannerCard.visibility = View.GONE
            questionnaireListHost.visibility = View.GONE
            questionnaireScroll.visibility = View.GONE
            bottomUploadDock.visibility = View.VISIBLE
        } else {
            clearLoadedClientState()
            noClientsAssignedText.text = t("no_clients_assigned")
            noClientsAssignedText.visibility = View.VISIBLE
            textView.visibility = View.GONE
            updateSessionMetrics()
            nextStepBannerCard.visibility = View.GONE
            questionnaireListHost.visibility = View.GONE
            questionnaireScroll.visibility = View.GONE
            bottomUploadDock.visibility = View.GONE
        }
    }

    private fun updateNextStepBanner() {
        if (!::nextStepBannerCard.isInitialized) return
        nextStepBannerCard.visibility = View.GONE
        nextStepActionButton.visibility = View.GONE
        nextStepActionButton.setOnClickListener(null)
    }

    private fun applyNextStepBanner(titleKey: String, bodyKey: String, action: NextStepAction) {
        nextStepEyebrow.text = hubFallback("Nächster Sitzungsschritt", "Next session step")
        nextStepTitle.text = when (action) {
            NextStepAction.UPLOAD -> hubFallback("Synchronisation offen", "Sync required")
            NextStepAction.REVIEW -> hubFallback("Score-Review offen", "Score review pending")
            NextStepAction.QUESTIONNAIRE -> hubFallback("Sitzung fortsetzen", "Continue session")
            NextStepAction.NONE -> hubFallback("Sitzung vollständig", "Session complete")
        }
        nextStepBody.text = when (action) {
            NextStepAction.UPLOAD -> hubFallback("Es gibt fertige lokale Sitzungsdaten, die hochgeladen werden müssen.", "Completed local session data is waiting to be uploaded.")
            NextStepAction.REVIEW -> hubFallback("Mindestens ein Score benötigt noch eine fachliche Prüfung.", "At least one score still needs counselor review.")
            NextStepAction.QUESTIONNAIRE -> hubFallback("Der nächste verfügbare Messpunkt ist bereit für diese Sitzung.", "The next available measure is ready for this session.")
            NextStepAction.NONE -> hubFallback("Alle verfügbaren Messpunkte für diesen Klienten sind abgeschlossen.", "All available measures for this client are complete.")
        }
        when (action) {
            NextStepAction.UPLOAD -> {
                nextStepActionButton.visibility = View.VISIBLE
                nextStepActionButton.text = t("upload")
                nextStepActionButton.setOnClickListener {
                    TokenStore.ensureLegacyLoginTimestamp(activity)
                    val existingToken = TokenStore.getToken(activity)
                    if (!TokenStore.isSessionValid(activity) || existingToken.isNullOrBlank()) {
                        activity.requestLogin()
                    } else {
                        openUploadOverview(token = existingToken, syncAfterClose = true)
                    }
                }
            }
            NextStepAction.REVIEW -> {
                nextStepActionButton.visibility = View.VISIBLE
                nextStepActionButton.text = t("review_scores")
                nextStepActionButton.setOnClickListener { openReviewScores() }
            }
            NextStepAction.QUESTIONNAIRE -> {
                nextStepActionButton.visibility = View.VISIBLE
                nextStepActionButton.text = hubFallback("Messpunkt anzeigen", "Show measure")
                nextStepActionButton.setOnClickListener { highlightNextAvailableQuestionnaire() }
            }
            NextStepAction.NONE -> {
                nextStepActionButton.visibility = View.GONE
                nextStepActionButton.setOnClickListener(null)
            }
        }
    }

    private fun nextAvailableQuestionnaireButton(): Button? =
        dynamicButtons.firstOrNull { button ->
            button.isEnabled && !isCompleted(button)
        }

    private fun highlightNextAvailableQuestionnaire() {
        val button = nextAvailableQuestionnaireButton() ?: return
        val row = button.parent as? View ?: button
        val scroll = questionnaireScroll as? androidx.core.widget.NestedScrollView
        val mb = button as? MaterialButton
        scroll?.post {
            val rect = Rect()
            row.getDrawingRect(rect)
            scroll.offsetDescendantRectToMyCoords(row, rect)
            scroll.smoothScrollTo(0, (rect.top - dp(12)).coerceAtLeast(0))
        }

        if (mb == null) return
        val originalStrokeWidth = mb.strokeWidth
        val originalStrokeColor = mb.strokeColor
        val originalTint = mb.backgroundTintList
        mb.strokeWidth = dp(4)
        mb.strokeColor = ColorStateList.valueOf(color(R.color.brand_purple))
        mb.backgroundTintList = ColorStateList.valueOf(color(R.color.brand_questionnaire_highlight_bg))
        mb.postDelayed({
            mb.strokeWidth = originalStrokeWidth
            mb.strokeColor = originalStrokeColor
            mb.backgroundTintList = originalTint
        }, 1400L)
    }

    /** Rebuilds the client selector after a background sync (e.g. app restart). */
    fun refreshClientSpinner() {
        if (!::clientSelectorRow.isInitialized) return
        setupClientSelector()
        updateQuestionnaireSectionVisibility()
        if (assignedClientsForUi().isEmpty()) return
        if (!isClientAssigned(selectedClientCode)) {
            clearLoadedClientState()
        }
    }

    private fun setupClientSelector() {
        val allClients = QuestionnaireCache.getAssignedClients(activity)
        val clients = allClients.filter { !it.equals(DevSettingsAccess.CLIENT_CODE, ignoreCase = true) }
        if (clients.isEmpty() && DevSettingsAccess.isAssigned(activity)) {
            GlobalValues.LAST_CLIENT_CODE = null
            GlobalValues.LOADED_CLIENT_CODE = null
            activity.markOpeningScreenReinit()
            activity.startActivity(Intent(activity, DevSettingsActivity::class.java))
            return
        }
        Log.d(TAG, "setupClientSelector: assigned clients = ${clients.size}")

        applyCoachDisplay()

        val prev = GlobalValues.LAST_CLIENT_CODE
        if (!prev.isNullOrBlank() && clients.none { it.equals(prev, ignoreCase = true) }) {
            clearLoadedClientState()
        }
        if (clients.isEmpty()) {
            clearLoadedClientState()
            selectedClientCode = ""
            updateClientDisplay()
        } else {
            val restore = when {
                !prev.isNullOrBlank() && clients.any { it.equals(prev, ignoreCase = true) } -> prev
                selectedClientCode.isNotBlank() && clients.any { it.equals(selectedClientCode, ignoreCase = true) } ->
                    selectedClientCode
                else -> clients.first()
            }
            selectClient(restore, userInitiated = false)
        }

        clientSelectorRow.setOnClickListener {
            if (clients.isEmpty()) return@setOnClickListener
            ClientPickerBottomSheet(
                activity = activity,
                allClients = clients,
                currentClient = selectedClientCode.takeIf { it.isNotBlank() },
                searchHint = t("client_code"),
            ) { code ->
                selectClient(code, userInitiated = true)
            }.show()
        }
    }

    private fun selectClient(code: String, userInitiated: Boolean) {
        selectedClientCode = code
        GlobalValues.LAST_CLIENT_CODE = code
        ClientRecentStore.record(activity, code)
        updateClientDisplay()
        if (userInitiated || GlobalValues.LOADED_CLIENT_CODE != code) {
            loadHandler?.triggerLoad()
        }
    }

    private fun updateClientDisplay() {
        clientCodeDisplay.text = selectedClientCode.ifBlank { "—" }
    }

    private fun setupLoadHandler() {
        loadHandler = LoadButtonHandler(
            activity = activity,
            clientCodeProvider = { selectedClientCode },
            onLoadStarted = { setQuestionnaireListLoading(true) },
            onLoadFinished = {
                setQuestionnaireListLoading(false)
                refreshPendingUploadBadge()
                if (!firstClientLoadDone) {
                    firstClientLoadDone = true
                    val callbacks = afterFirstClientLoadCallbacks.toList()
                    afterFirstClientLoadCallbacks.clear()
                    callbacks.forEach { it() }
                }
            },
            languageIDProvider = { languageID },
            questionnaireEntriesProvider = { questionnaireEntries },
            dynamicButtonsProvider = { dynamicButtons },
            onCompletedQuestionnaireIds = { ids ->
                completedQuestionnaireIds.clear()
                completedQuestionnaireIds.addAll(ids)
            },
            updateButtonTexts = { applyUpdateButtonTexts(force = false) },
            setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = true, force = false) },
            updateMainButtonsState = { /* no-op: no load/save buttons to update */ },
            onPendingUploadIds = { pendingUploadQuestionnaireIds = it },
        )
    }

    private fun setQuestionnaireListLoading(loading: Boolean) {
        if (!::questionnaireListLoading.isInitialized) return
        ScreenLoading.setVisible(
            questionnaireListLoading,
            loading,
            questionnaireScroll,
            nextStepBannerCard,
        )
    }

    fun refreshPendingUploadBadge() {
        if (!::uploadButton.isInitialized) return
        CoroutineScope(Dispatchers.Main).launch {
            val count = withContext(Dispatchers.IO) {
                PendingUploadRepository.countUploadReady(activity)
            }
            val base = t("upload")
            uploadButton.text = if (count > 0) "$base ($count)" else base
            uploadButton.contentDescription = uploadButton.text
            uploadButton.isEnabled = true
            uploadButton.alpha = if (count > 0) 1f else 0.55f
            if (::sessionMetricSyncValue.isInitialized) {
                sessionMetricSyncValue.text = if (count > 0) count.toString() else "OK"
            }
        }
    }


    private fun rebuildLanguageSpinnerAdapter() {
        val labels = AppLanguages.spinnerLabels(languageID)
        val selected = labels.getOrNull(AppLanguages.indexOf(languageID))
        ViewUtils.setupLanguagePickerSpinner(activity, spinner, labels, selected)
    }

    private fun setupLanguageSpinner() {
        suppressLanguageCallback = true
        rebuildLanguageSpinnerAdapter()
        spinner.setSelection(AppLanguages.indexOf(languageID))
        suppressLanguageCallback = false
        spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
                if (suppressLanguageCallback) return
                val newLang = AppLanguages.ids[position]
                if (newLang == languageID) return
                onUiLanguageChanged(newLang)
            }
            override fun onNothingSelected(parent: AdapterView<*>) {}
        }
    }

    private fun onUiLanguageChanged(newLang: String) {
        languageID = newLang
        AppLanguageStore.set(activity, newLang)
        suppressLanguageCallback = true
        rebuildLanguageSpinnerAdapter()
        spinner.setSelection(AppLanguages.indexOf(languageID))
        suppressLanguageCallback = false
        applyHubLabels()
        applyUpdateButtonTexts(force = true)
    }

    private fun applyUpdateButtonTexts(force: Boolean) {
        if (uiFreeze && !force) return
        applyHubLabels()
        updateStatusStrip()
        questionnaireFiles.forEach { (button, fileName) ->
            val entry = questionnaireEntries.firstOrNull { it.file == fileName }
            val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
            val titleText = entry?.serverName?.takeIf { it.isNotBlank() } ?: t(key)
            val parts = cardParts[button] ?: return@forEach
            parts.title.text = titleText
            val completed = isCompleted(button)
            val enabled = button.isEnabled
            val locked = !enabled && !completed
            QuestionnaireCardUi.applyDefaultCardTexts(activity, parts)
            QuestionnaireCardUi.setClickableStroke(activity, button, enabled)
            if (locked) {
                QuestionnaireCardUi.setLockedAppearance(activity, button, true)
                QuestionnaireCardUi.applyLockedCardTexts(activity, parts, t("locked"))
                lockHintForEntry(entry)?.let { hint ->
                    parts.subtitle.visibility = View.VISIBLE
                    parts.subtitle.text = hint
                } ?: run {
                    parts.subtitle.visibility = View.GONE
                    parts.subtitle.text = ""
                }
            } else {
                QuestionnaireCardUi.setLockedAppearance(activity, button, false)
                parts.title.setTextColor(color(R.color.brand_text_dark))
                parts.subtitle.setTextColor(color(R.color.brand_text_muted))
                QuestionnaireCardUi.applyEmphasisTint(activity, button, emphasize = enabled || completed)
                parts.subtitle.visibility = View.GONE
                parts.subtitle.text = ""
            }
            (button.parent as? View)?.requestLayout()
            val chipKey = chipTextKey(completed, enabled, fileName)
            when {
                completed -> QuestionnaireCardUi.applyChipStatus(
                    activity,
                    parts,
                    t(chipKey),
                    if (chipKey == "questionnaire_chip_edit_pending") R.drawable.bg_chip_amber
                    else R.drawable.bg_chip_green,
                )
                enabled -> QuestionnaireCardUi.applyChipStatus(
                    activity,
                    parts,
                    t(chipKey),
                    R.drawable.bg_chip_amber,
                )
                locked -> QuestionnaireCardUi.applyLockedChipStyle(activity, parts, t(chipKey))
            }
        }
        reviewScoresButton.text = t("review_scores")
        reviewScoresButton.visibility = View.GONE
        uploadButton.text = t("upload")
        applyHubLabels()
        applyCoachDisplay()
        textView.text = hubFallback("Sitzungsinstrumente", "Session measures")
        updateSessionMetrics()
        updateNextStepBanner()
    }

    private fun applySetButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean, force: Boolean) {
        if (uiFreeze && !force) return
        questionnaireFiles.keys.forEach { button ->
            val completed = isCompleted(button)
            val isAllowed = enabledButtons.contains(button)
            val shouldEnable = isAllowed
            val locked = !shouldEnable && !completed
            button.isEnabled = shouldEnable
            button.alpha = if (completed || shouldEnable) 1.0f else 0.6f
            QuestionnaireCardUi.setClickableStroke(activity, button, shouldEnable)
            cardParts[button]?.let { parts ->
                val entry = questionnaireEntries.firstOrNull {
                    questionnaireFiles[button] == it.file
                }
                val fileName = questionnaireFiles[button] ?: ""
                QuestionnaireCardUi.applyDefaultCardTexts(activity, parts)
                if (locked) {
                    QuestionnaireCardUi.setLockedAppearance(activity, button, true)
                    QuestionnaireCardUi.applyLockedCardTexts(activity, parts, t("locked"))
                    lockHintForEntry(entry)?.let { hint ->
                        parts.subtitle.visibility = View.VISIBLE
                        parts.subtitle.text = hint
                    } ?: run {
                        parts.subtitle.visibility = View.GONE
                        parts.subtitle.text = ""
                    }
                } else {
                    QuestionnaireCardUi.setLockedAppearance(activity, button, false)
                    parts.title.setTextColor(color(R.color.brand_text_dark))
                    parts.subtitle.setTextColor(color(R.color.brand_text_muted))
                    QuestionnaireCardUi.applyEmphasisTint(
                        activity,
                        button,
                        emphasize = shouldEnable || completed,
                    )
                }
                val chipKey = chipTextKey(completed, shouldEnable, fileName)
                when {
                    completed -> QuestionnaireCardUi.applyChipStatus(
                        activity,
                        parts,
                        t(chipKey),
                        if (chipKey == "questionnaire_chip_edit_pending") R.drawable.bg_chip_amber
                        else R.drawable.bg_chip_green,
                    )
                    shouldEnable -> QuestionnaireCardUi.applyChipStatus(
                        activity,
                        parts,
                        t(chipKey),
                        R.drawable.bg_chip_amber,
                    )
                    locked -> QuestionnaireCardUi.applyLockedChipStyle(activity, parts, t(chipKey))
                }
                (button.parent as? View)?.requestLayout()
            }
        }
        updateNextStepBanner()
    }

    private fun onQuestionnaireCardClicked(button: Button) {
        val fileName = questionnaireFiles[button] ?: return
        Log.d(
            TAG,
            "questionnaire tap: $fileName enabled=${button.isEnabled} completed=${isCompleted(button)}"
        )
        if (!button.isEnabled) {
            val entry = questionnaireEntries.firstOrNull { questionnaireFiles[button] == it.file }
            val toastText = lockHintForEntry(entry) ?: t("locked")
            Toast.makeText(activity, toastText, Toast.LENGTH_SHORT).show()
            return
        }
        val clientCode = selectedClientCode
        if (!isClientAssigned(clientCode)) {
            clearLoadedClientState()
            Toast.makeText(activity, t("no_profile"), Toast.LENGTH_LONG).show()
            return
        }
        GlobalValues.LAST_CLIENT_CODE = clientCode
        GlobalValues.LOADED_CLIENT_CODE = clientCode
        QuestionnaireValidation.configErrorKeyForFile(activity, fileName)?.let { errorKey ->
            Log.w(TAG, "questionnaire blocked: $fileName reason=$errorKey")
            QuestionnaireValidation.showConfigError(activity, languageID, errorKey)
            return
        }
        Log.d(TAG, "opening questionnaire: $fileName")
        val questionnaire: QuestionnaireBase<*> = when (AbTestSettingsStore.effectiveVariant(activity)) {
            "A" -> QuestionnaireGeneric(fileName)
            else -> QuestionnaireAllInOne(fileName)
        }
        startQuestionnaire(questionnaire)
        applySetButtonsEnabled(dynamicButtons.filter { it == button }, allowCompleted = false, force = false)
    }

    private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
        nextStepJob?.cancel()
        activity.startQuestionnaire(questionnaire, languageID)
    }

    fun onBackPressed(): Boolean = false

    private fun setupReviewScoresButton() {
        reviewScoresButton.text = t("review_scores")
        reviewScoresButton.visibility = View.GONE
        reviewScoresButton.setOnClickListener { openReviewScores() }
    }

    private fun openReviewScores() {
        TokenStore.ensureLegacyLoginTimestamp(activity)
        val existingToken = TokenStore.getToken(activity)
        if (!TokenStore.isSessionValid(activity) || existingToken.isNullOrBlank()) {
            activity.requestLogin()
            return
        }

        nextStepJob?.cancel()
        ReviewScoresHandler(
            activity = activity,
            token = existingToken,
            languageIDProvider = { languageID },
            onLanguageChanged = { newLang ->
                languageID = newLang
                AppLanguageStore.set(activity, newLang)
            },
            onUploadRequested = {
                openUploadOverview(token = TokenStore.getToken(activity), syncAfterClose = true)
            },
            onClose = { init() },
        ).show()
    }

    private fun setupUploadButton() {
        reviewScoresButton.visibility = View.GONE
        uploadButton.text = t("upload")
        uploadButton.setOnClickListener {
            TokenStore.ensureLegacyLoginTimestamp(activity)
            val existingToken = TokenStore.getToken(activity)
            if (!TokenStore.isSessionValid(activity)) {
                activity.requestLogin()
                return@setOnClickListener
            }

            CoroutineScope(Dispatchers.Main).launch {
                val readyCount = withContext(Dispatchers.IO) {
                    PendingUploadRepository.countUploadReady(activity)
                }
                if (readyCount == 0) {
                    android.widget.Toast.makeText(
                        activity,
                        hubFallback(
                            "Upload ist erst möglich, wenn alle Messpunkte der Sitzung abgeschlossen sind.",
                            "Upload is only available after all measures in the session are complete.",
                        ),
                        android.widget.Toast.LENGTH_LONG,
                    ).show()
                    return@launch
                }
                openUploadOverview(token = existingToken!!, syncAfterClose = true)
            }
        }
    }

    private fun openUploadOverview(
        token: String? = TokenStore.getToken(activity),
        syncAfterClose: Boolean,
    ) {
        val existingToken = token
        if (existingToken.isNullOrBlank() || !TokenStore.isSessionValid(activity)) {
            activity.requestLogin()
            return
        }

        nextStepJob?.cancel()
        GlobalValues.LAST_CLIENT_CODE = selectedClientCode
        UploadOverviewHandler(
            activity = activity,
            token = existingToken,
            languageIDProvider = { languageID },
            onLanguageChanged = { newLang ->
                languageID = newLang
                AppLanguageStore.set(activity, newLang)
            },
            onClose = { uploaded ->
                refreshOpeningUi()
                refreshPendingUploadBadge()
                if (uploaded && syncAfterClose && NetworkUtils.isOnline(activity) && TokenStore.isSessionValid(activity)) {
                    TokenStore.getToken(activity)?.takeIf { it.isNotBlank() }?.let {
                        performSyncWithOverlay(
                            token = it,
                            showSuccessToast = false,
                            onFinished = { refreshPendingUploadBadge() },
                        )
                    }
                }
            },
        ).show()
    }

    private fun isCompleted(button: Button): Boolean {
        val fileName = questionnaireFiles[button] ?: return false
        return completedQuestionnaireIds.any { id ->
            fileName.contains(id, ignoreCase = true) || id.contains(fileName, ignoreCase = true)
        }
    }

    private fun updateStatusStrip() {
        if (!::statusChipContainer.isInitialized) return
        val online = NetworkUtils.isOnline(activity)
        statusOnline.text = if (online) t("online") else t("offline")
        statusChipContainer.setBackgroundResource(
            if (online) R.drawable.bg_status_chip_online else R.drawable.bg_status_chip_offline,
        )
        statusOnline.setTextColor(
            if (online) color(R.color.brand_status_chip_online_text)
            else color(R.color.brand_status_chip_offline_text),
        )
        statusDot.imageTintList = ColorStateList.valueOf(
            if (online) color(R.color.brand_status_online) else color(R.color.brand_status_offline),
        )
    }

    fun refreshHeaderStatusLive() {
        applyUpdateButtonTexts(force = true)
    }

    private fun dp(v: Int): Int = ViewUtils.dp(activity, v)

    private fun setupCoachDisplay() {
        // Long-press on the coach name is the hidden entry point for dev settings.
        coachNameDisplay.setOnLongClickListener {
            if (DevSettingsAccess.isAssigned(activity)) {
                GlobalValues.LAST_CLIENT_CODE = null
                GlobalValues.LOADED_CLIENT_CODE = null
                activity.markOpeningScreenReinit()
                activity.startActivity(Intent(activity, DevSettingsActivity::class.java))
            }
            true
        }
    }

}
