/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.ui.notifications

import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.runicgateway.app.R
import com.runicgateway.app.core.push.PushManager
import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.data.api.dto.NotificationStreamDto
import com.runicgateway.app.data.repository.NotificationsRepository
import com.runicgateway.app.data.repository.PlayerShardRepository
import com.runicgateway.app.ui.UiState
import com.runicgateway.app.ui.toUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
 * Drives the Notifications settings screen (PLAN.md §11, M7 Part 2 work item 6):
 * the stream catalog with per-stream toggles bound to
 * `GET/PUT /auth/me/notifications/subscriptions`. A **personal** stream is greyed
 * until the user has a linked game account (§11), and turning the opt-in set
 * non-empty/empty drives the [PushManager] to register/unregister the device.
 */
@HiltViewModel
class NotificationsViewModel @Inject constructor(
    private val notifications: NotificationsRepository,
    private val playerShard: PlayerShardRepository,
    private val pushManager: PushManager,
) : ViewModel() {

    data class Feedback(val ok: Boolean, @param:StringRes val messageRes: Int)

    data class State(
        val catalog: UiState<List<NotificationStreamDto>> = UiState.Loading,
        val subscribed: Set<String> = emptySet(),
        /** Whether the user has ≥1 linked game account — personal streams need it. */
        val hasLinkedAccount: Boolean = false,
        /** Whether this shard advertises a push relay at all (else the screen says so). */
        val supported: Boolean = true,
        val busy: Boolean = false,
        val feedback: Feedback? = null,
    )

    private val _state = MutableStateFlow(State())
    val state: StateFlow<State> = _state.asStateFlow()

    init {
        viewModelScope.launch {
            pushManager.supported.collect { supported -> _state.update { it.copy(supported = supported) } }
        }
        load()
    }

    fun load() {
        _state.update { it.copy(catalog = UiState.Loading) }
        viewModelScope.launch {
            val catalog = notifications.streams().let { result ->
                when (result) {
                    is ApiResult.Ok -> ApiResult.Ok(result.data.streams)
                    is ApiResult.HttpError -> result
                    is ApiResult.NetworkError -> result
                }
            }
            _state.update { it.copy(catalog = catalog.toUiState()) }

            when (val subs = notifications.subscriptions()) {
                is ApiResult.Ok -> _state.update { it.copy(subscribed = subs.data.streams.toSet()) }
                else -> Unit
            }
            // A linked game account gates the personal streams; failure → treat as none.
            val linked = (playerShard.accounts() as? ApiResult.Ok)?.data?.isNotEmpty() == true
            _state.update { it.copy(hasLinkedAccount = linked) }
        }
    }

    fun clearFeedback() = _state.update { it.copy(feedback = null) }

    /** Toggle [stream]; refuses a personal stream with no linked account. */
    fun setSubscribed(stream: NotificationStreamDto, on: Boolean) {
        val s = _state.value
        if (s.busy) return
        if (on && !streamSelectable(stream, s.hasLinkedAccount)) return
        val next = if (on) s.subscribed + stream.id else s.subscribed - stream.id

        _state.update { it.copy(busy = true, feedback = null) }
        viewModelScope.launch {
            when (val result = notifications.setSubscriptions(next.toList())) {
                is ApiResult.Ok -> {
                    val stored = result.data.streams.toSet()
                    _state.update { it.copy(subscribed = stored) }
                    reconcilePush(stored)
                }
                is ApiResult.NetworkError -> finish(false, R.string.error_network)
                is ApiResult.HttpError -> finish(false, R.string.notifications_save_error)
            }
        }
    }

    /**
     * Register or unregister the device to match the opted-in set (PLAN.md §11:
     * register when signed-in + subscribed, unregister when the set empties).
     */
    private suspend fun reconcilePush(subscribed: Set<String>) {
        if (subscribed.isEmpty()) {
            pushManager.disable()
            finish(true, R.string.notifications_all_off)
            return
        }
        when (val res = pushManager.enable()) {
            is PushManager.PushResult.Enabled -> finish(true, R.string.notifications_saved)
            is PushManager.PushResult.Unsupported -> finish(false, R.string.notifications_unsupported)
            is PushManager.PushResult.NotSignedIn -> finish(false, R.string.notifications_save_error)
            is PushManager.PushResult.Failed ->
                finish(false, if (res.status == 400) R.string.notifications_relay_error else R.string.notifications_save_error)
        }
    }

    private fun finish(ok: Boolean, @StringRes messageRes: Int) =
        _state.update { it.copy(busy = false, feedback = Feedback(ok, messageRes)) }
}

/**
 * Whether a stream's toggle is selectable for a user: a personal stream needs a
 * linked game account (PLAN.md §11). Pure so the gating is unit-tested without Compose.
 */
fun streamSelectable(stream: NotificationStreamDto, hasLinkedAccount: Boolean): Boolean =
    !stream.requiresLinkedAccount || hasLinkedAccount
