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

import android.content.Context
import com.runicgateway.app.core.auth.Session
import com.runicgateway.app.core.auth.SessionManager
import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.data.repository.NotificationsRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Orchestrates the app's opt-in push lifecycle (PLAN.md §11, M7 Part 2 work item 5):
 * mint/keep the ntfy topic, register/unregister the device endpoint with the backend,
 * and start/stop the foreground [PushService] — all keyed to the user's opt-in and
 * the session. The endpoint the app registers is its own topic URL on the shard's
 * ntfy (the embedded-distributor design, work item 1).
 *
 * Lifecycle rules:
 *  - register only when **signed in** and the shard advertises a relay (`ntfyUrl`);
 *  - a **sign-out** stops the service and forgets the ephemeral registration but keeps
 *    the opt-in intent, so push re-registers on the next sign-in (mirrors the M3 token
 *    teardown, and covers logout / dead-refresh / server switch uniformly via the
 *    session-state observer);
 *  - a **relay/base-URL change** re-registers on the new host with a fresh topic.
 */
@Singleton
class PushManager @Inject constructor(
    @param:ApplicationContext private val context: Context,
    private val prefs: PushPreferences,
    private val notifications: NotificationsRepository,
    private val sessionManager: SessionManager,
) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    /** Whether the user has push turned on (drives the Notifications screen). */
    val enabled: Flow<Boolean> = prefs.enabled

    /** Whether this shard advertises a push relay at all (null ntfyUrl → unsupported). */
    val supported: Flow<Boolean> = prefs.ntfyUrl.map { !it.isNullOrBlank() }

    init {
        // Uniform teardown/resume across every auth transition: logout, dead-refresh
        // sign-out, and server switch all land on SignedOut; a fresh login re-asserts.
        scope.launch {
            sessionManager.state.collect { s ->
                when (s) {
                    is Session.SignedOut -> localTeardown()
                    is Session.SignedIn -> maybeResume()
                }
            }
        }
    }

    /** Record the shard's client-facing ntfy base URL (from `/public/settings`). */
    suspend fun setNtfyUrl(url: String?) {
        val previous = prefs.snapshot().ntfyUrl
        prefs.setNtfyUrl(url)
        // The relay host arriving (or changing) is what unblocks a pending resume.
        if (!url.isNullOrBlank() && url != previous) maybeResume()
    }

    /**
     * Turn push on (idempotent): ensure a topic on the current relay, register its
     * endpoint with the backend, persist, and start the foreground service. Called
     * when the user opts into ≥1 stream.
     */
    suspend fun enable(): PushResult = register(setIntent = true)

    /** Turn push off (user opted out of every stream): clear intent + deregister. */
    suspend fun disable() {
        prefs.setEnabled(false)
        deregisterDevice()
    }

    /**
     * Deregister this device on an explicit sign-out / server switch, while the bearer
     * is still valid, so no orphan device row is left behind. Keeps the opt-in intent
     * (and ntfyUrl) so push re-registers on the next sign-in. Call this *before* the
     * session is torn down.
     */
    suspend fun deregisterDevice() {
        val snap = prefs.snapshot()
        snap.deviceId?.let { notifications.deleteDevice(it) } // best-effort
        stopService()
        prefs.clearRegistration()
    }

    /** Re-assert registration if the user is opted in and the shard supports push. */
    private suspend fun maybeResume() {
        val snap = prefs.snapshot()
        if (snap.enabled && sessionManager.isSignedIn && !snap.ntfyUrl.isNullOrBlank()) {
            register(setIntent = false)
        }
    }

    private suspend fun register(setIntent: Boolean): PushResult {
        if (!sessionManager.isSignedIn) return PushResult.NotSignedIn
        val snap = prefs.snapshot()
        val ntfyUrl = snap.ntfyUrl
        if (ntfyUrl.isNullOrBlank()) return PushResult.Unsupported

        // Reuse an existing topic only if its endpoint still sits on the current relay
        // origin; otherwise (first run, or a server switch) mint a fresh unguessable one.
        val base = ntfyUrl.trimEnd('/')
        val topic = snap.topic?.takeIf { snap.endpoint?.startsWith("$base/") == true }
            ?: NtfyTopic.generate()
        val endpoint = NtfyTopic.endpointUrl(ntfyUrl, topic) ?: return PushResult.Unsupported

        return when (val res = notifications.registerDevice(endpoint, PLATFORM)) {
            is ApiResult.Ok -> {
                prefs.setRegistration(topic, endpoint, res.data.id)
                if (setIntent) prefs.setEnabled(true)
                startService()
                PushResult.Enabled
            }
            // 400 = endpoint origin isn't on the shard's ntfy allow-set (misconfigured relay).
            is ApiResult.HttpError -> PushResult.Failed(res.status)
            is ApiResult.NetworkError -> PushResult.Failed(null)
        }
    }

    /** Local-only teardown on sign-out — no backend DELETE (the bearer may be dead). */
    private suspend fun localTeardown() {
        stopService()
        prefs.clearRegistration()
    }

    private fun startService() = runCatching { PushService.start(context) }
    private fun stopService() = runCatching { PushService.stop(context) }

    /** The outcome of enabling push, surfaced to the Notifications screen. */
    sealed interface PushResult {
        data object Enabled : PushResult

        /** This shard advertises no push relay (`/public/settings.push.ntfyUrl` is null). */
        data object Unsupported : PushResult
        data object NotSignedIn : PushResult

        /** Registration failed — [status] 400 = relay off the allow-set; null = network. */
        data class Failed(val status: Int?) : PushResult
    }

    private companion object {
        const val PLATFORM = "android"
    }
}
