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

import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.core.result.safeApiCall
import com.runicgateway.app.data.api.MeApi
import com.runicgateway.app.data.api.dto.ChangePasswordRequest
import com.runicgateway.app.data.api.dto.ChangeUsernameRequest
import com.runicgateway.app.data.api.dto.LinkedIdentityDto
import com.runicgateway.app.data.api.dto.PlayerAccountDto
import com.runicgateway.app.data.api.dto.RecoveryCodesDto
import com.runicgateway.app.data.api.dto.RecoveryGenerateRequest
import com.runicgateway.app.data.api.dto.RecoveryStatusDto
import com.runicgateway.app.data.api.dto.TotpCodeRequest
import com.runicgateway.app.data.api.dto.TotpSetupDto
import com.runicgateway.app.data.api.dto.TotpStateDto
import com.runicgateway.app.data.api.dto.TrustDeviceRequest
import com.runicgateway.app.data.api.dto.TrustedDeviceDto
import com.runicgateway.app.data.api.dto.TrustedDeviceLimitDto
import com.runicgateway.app.data.api.dto.UsernameResponse
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.json.Json
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Self-service account management over the role-agnostic `/auth/me/account*`
 * surface (PLAN.md §6.3, §6.4). Every call returns a typed [ApiResult] so the
 * screens can map known statuses (409 taken, 400 wrong password / invalid code,
 * 429 rate-limited) to friendly copy without a repository ever throwing (§7).
 */
@Singleton
class AccountRepository @Inject constructor(
    private val api: MeApi,
    private val json: Json,
) {
    suspend fun getAccount(): ApiResult<PlayerAccountDto> = safeApiCall { api.getAccount() }

    suspend fun changeUsername(username: String): ApiResult<UsernameResponse> =
        safeApiCall { api.changeUsername(ChangeUsernameRequest(username)) }

    /** [currentPassword] is null only for an SSO account setting its first password. */
    suspend fun changePassword(newPassword: String, currentPassword: String?): ApiResult<Unit> =
        safeApiCall { api.changePassword(ChangePasswordRequest(newPassword, currentPassword)) }

    suspend fun totpSetup(): ApiResult<TotpSetupDto> = safeApiCall { api.totpSetup() }

    suspend fun totpEnable(code: String): ApiResult<TotpStateDto> =
        safeApiCall { api.totpEnable(TotpCodeRequest(code)) }

    suspend fun totpDisable(code: String): ApiResult<TotpStateDto> =
        safeApiCall { api.totpDisable(TotpCodeRequest(code)) }

    suspend fun identities(): ApiResult<List<LinkedIdentityDto>> = safeApiCall { api.identities() }

    suspend fun unlinkIdentity(provider: String): ApiResult<Unit> =
        safeApiCall { api.unlinkIdentity(provider) }

    // ── Trusted devices (TRUSTED_DEVICES_MFA.md) ───────────────────────────

    suspend fun trustedDevices(): ApiResult<List<TrustedDeviceDto>> =
        safeApiCall { api.trustedDevices() }

    /** The distinct outcomes of trusting the current device — the cap is a first-class case. */
    sealed interface TrustOutcome {
        /** Trusted; [trustToken] is the opaque token to persist (native). */
        data class Trusted(val trustToken: String?) : TrustOutcome

        /** At the per-user cap — [devices] must be pruned before retrying. */
        data class LimitReached(val devices: List<TrustedDeviceDto>) : TrustOutcome
        data object NetworkError : TrustOutcome
        data object ServerError : TrustOutcome
    }

    /**
     * Trust the current device. Reads the raw response so the `409 { error, devices }`
     * cap body survives (a thrown [retrofit2.HttpException] would discard it).
     */
    suspend fun trustThisDevice(deviceName: String? = null): TrustOutcome {
        val response = try {
            api.trustThisDevice(TrustDeviceRequest(deviceName))
        } catch (e: CancellationException) {
            throw e
        } catch (_: IOException) {
            return TrustOutcome.NetworkError
        } catch (_: Exception) {
            return TrustOutcome.ServerError
        }
        if (response.isSuccessful) {
            return TrustOutcome.Trusted(response.body()?.trustToken)
        }
        if (response.code() == 409) {
            val devices = runCatching {
                val raw = response.errorBody()?.string()
                if (raw.isNullOrBlank()) emptyList()
                else json.decodeFromString<TrustedDeviceLimitDto>(raw).devices
            }.getOrDefault(emptyList())
            return TrustOutcome.LimitReached(devices)
        }
        return TrustOutcome.ServerError
    }

    suspend fun revokeTrustedDevice(id: Long): ApiResult<Boolean> =
        safeApiCall { api.revokeTrustedDevice(id).revoked }

    suspend fun revokeAllTrustedDevices(): ApiResult<Int> =
        safeApiCall { api.revokeAllTrustedDevices().revoked }

    // ── Recovery (backup) codes ────────────────────────────────────────────

    suspend fun recoveryCodesStatus(): ApiResult<RecoveryStatusDto> =
        safeApiCall { api.recoveryCodesStatus() }

    /** Regenerate the single-use codes (password step-up). Returned once — never stored. */
    suspend fun generateRecoveryCodes(currentPassword: String?): ApiResult<RecoveryCodesDto> =
        safeApiCall { api.generateRecoveryCodes(RecoveryGenerateRequest(currentPassword)) }
}
