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

import com.runicgateway.app.core.auth.SessionManager
import com.runicgateway.app.core.auth.TrustTokenStore
import com.runicgateway.app.core.net.BaseUrlHolder
import com.runicgateway.app.core.net.ServerUrl
import com.runicgateway.app.core.prefs.ServerPreferences
import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.core.result.safeApiCall
import com.runicgateway.app.data.api.PublicApi
import com.runicgateway.app.data.api.dto.StatusDto
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Owns the shard website base-URL lifecycle (PLAN.md §3): restoring a saved URL
 * on launch, validating + persisting a candidate on the first-run connect
 * screen, and the hard reset performed by a Settings → Server switch.
 */
@Singleton
class ConnectionRepository @Inject constructor(
    private val api: PublicApi,
    private val prefs: ServerPreferences,
    private val baseUrlHolder: BaseUrlHolder,
    private val sessionManager: SessionManager,
    private val trustTokenStore: TrustTokenStore,
    private val shardFeaturesRepository: ShardFeaturesRepository,
    private val pushManager: com.runicgateway.app.core.push.PushManager,
    private val config: com.runicgateway.app.core.AppConfig,
) {

    /** Outcome of validating a candidate base URL against a live site. */
    sealed interface ProbeResult {
        data class Success(val status: StatusDto) : ProbeResult
        data class InvalidUrl(val reason: ServerUrl.Reason) : ProbeResult

        /** Reachable and 2xx, but not a Runic Gateway backend (wrong version identity). */
        data object NotRunicGateway : ProbeResult

        /**
         * A Runic Gateway backend, but speaking an API version this app build does
         * not support (§3 version guard) — refuse rather than mis-render. [serverApi]
         * is what the site reported; [supportedApi] is what this app speaks.
         */
        data class VersionMismatch(val serverApi: String, val supportedApi: String) : ProbeResult
        data class ServerError(val status: Int) : ProbeResult
        data class Unreachable(val cause: Throwable) : ProbeResult
    }

    /** True once a saved base URL has been loaded into the holder. */
    val isConnected: Boolean get() = baseUrlHolder.current != null

    /**
     * Restore any saved base URL into the holder on launch. Returns true if the
     * app already has a configured shard site (skip the connect screen).
     */
    suspend fun restore(): Boolean {
        val saved = prefs.currentBaseUrl()?.toHttpUrlOrNull()
        baseUrlHolder.set(saved)
        return saved != null
    }

    /**
     * Validate [rawUrl], and on success persist it and activate it for all
     * subsequent API calls. Insecure HTTP is allowed only in debug builds
     * (local dev against 127.0.0.1); release builds require HTTPS.
     */
    suspend fun probeAndConnect(rawUrl: String): ProbeResult {
        val normalized = when (val r = ServerUrl.normalize(rawUrl, allowInsecureHttp = config.allowInsecureHttp)) {
            is ServerUrl.Result.Invalid -> return ProbeResult.InvalidUrl(r.reason)
            is ServerUrl.Result.Valid -> r.url
        }

        val statusUrl = normalized.resolve("api/v1/public/status")?.toString()
            ?: return ProbeResult.InvalidUrl(ServerUrl.Reason.MALFORMED)

        return when (val result = safeApiCall { api.probeStatus(statusUrl) }) {
            is ApiResult.Ok -> when (val verdict = evaluateVersion(result.data.version)) {
                is VersionVerdict.Ok -> {
                    prefs.setBaseUrl(normalized.toString())
                    baseUrlHolder.set(normalized)
                    ProbeResult.Success(result.data)
                }
                is VersionVerdict.NotRunicGateway -> ProbeResult.NotRunicGateway
                is VersionVerdict.Mismatch ->
                    ProbeResult.VersionMismatch(serverApi = verdict.serverApi, supportedApi = SUPPORTED_API)
            }
            is ApiResult.HttpError -> ProbeResult.ServerError(result.status)
            is ApiResult.NetworkError -> ProbeResult.Unreachable(result.cause)
        }
    }

    /**
     * Hard reset for a Settings → Server switch (§3): sign out (clear stored
     * tokens), clear the saved URL, and deactivate it — the app returns to a
     * signed-out state against the new host.
     */
    suspend fun disconnect() {
        // Deregister the push endpoint on the current (old) host while still authed,
        // then clear the shard's ntfy URL — the new host advertises its own (§11).
        try {
            pushManager.deregisterDevice()
        } catch (_: Exception) {
            // Best-effort; the reset proceeds regardless.
        }
        pushManager.setNtfyUrl(null)
        sessionManager.onSignedOut()
        // The trust token is bound to the old host — drop it so we don't replay it
        // against a different shard (it survives a plain logout, but not a host switch).
        trustTokenStore.clear()
        // Shard visibility is the OLD host's answer. Sign-out alone would not clear it:
        // a switch between two signed-out hosts changes no session, so nothing else
        // invalidates the cache and the new shard would inherit the old one's menu.
        shardFeaturesRepository.invalidate()
        prefs.clear()
        baseUrlHolder.set(null)
    }

    /** The pure outcome of inspecting a probed site's [VersionDto] (§3 version guard). */
    internal sealed interface VersionVerdict {
        data object Ok : VersionVerdict
        data object NotRunicGateway : VersionVerdict
        data class Mismatch(val serverApi: String) : VersionVerdict
    }

    internal companion object {
        const val RUNIC_SERVICE_ID = "runic-gateway"

        /** The backend API major version this app build speaks (matches `/public/version` `api`). */
        const val SUPPORTED_API = "v1"

        /**
         * Decide whether a probed site is a Runic Gateway backend this app can talk
         * to. Pure (no I/O) so it is unit-testable without a live site. Lenient on a
         * blank `api` (an older backend that predates version surfacing); refuses only
         * an API version we positively know we can't parse (e.g. a future `v2`).
         */
        fun evaluateVersion(
            version: com.runicgateway.app.data.api.dto.VersionDto,
            supportedApi: String = SUPPORTED_API,
        ): VersionVerdict {
            if (!version.service.trim().equals(RUNIC_SERVICE_ID, ignoreCase = true)) {
                return VersionVerdict.NotRunicGateway
            }
            val serverApi = version.api.trim()
            return when {
                serverApi.isEmpty() -> VersionVerdict.Ok
                serverApi.equals(supportedApi, ignoreCase = true) -> VersionVerdict.Ok
                else -> VersionVerdict.Mismatch(serverApi)
            }
        }
    }
}
