package lu.rewardflow.terminal.data.repository

import at.favre.lib.crypto.bcrypt.BCrypt
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import kotlinx.coroutines.flow.first
import lu.rewardflow.terminal.data.api.LoyaltyApi
import lu.rewardflow.terminal.data.model.PinForDeviceItem
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Source of truth for staff PINs on the device.
 *
 * Refreshes from ``GET /api/v1/store/loyalty/pins/for-device`` (which only
 * accepts paired-device JWTs and includes the bcrypt ``pin_hash``) and
 * caches the serialized list in DataStore so the PIN screen can render
 * before the network call completes — and stay usable when the tablet has
 * no signal at all.
 *
 * [verifyPin] does the bcrypt comparison locally so the tablet doesn't
 * round-trip the typed PIN to the server. Lockout state from the server
 * (``is_locked``) is honored, but failed-attempt reporting back to the
 * server's lockout machinery is a TODO for a follow-up.
 */
@Singleton
class StaffPinRepository @Inject constructor(
    private val api: LoyaltyApi,
    private val configRepository: DeviceConfigRepository,
    moshi: Moshi,
) {

    private val listAdapter: JsonAdapter<List<PinForDeviceItem>> = moshi.adapter(
        Types.newParameterizedType(List::class.java, PinForDeviceItem::class.java)
    )

    /** Hit the server, persist the result, return the freshly fetched list. */
    suspend fun refresh(): List<PinForDeviceItem> {
        val response = api.listPinsForDevice()
        val pins = response.pins
        configRepository.saveStaffPins(listAdapter.toJson(pins))
        return pins
    }

    /** Last cached list. Empty if the device has never synced. */
    suspend fun cached(): List<PinForDeviceItem> {
        val raw = configRepository.staffPinsJson.first() ?: return emptyList()
        return runCatching { listAdapter.fromJson(raw) ?: emptyList() }
            .getOrDefault(emptyList())
    }

    /** Cached if available, otherwise hit the network. */
    suspend fun listOrRefresh(): List<PinForDeviceItem> {
        val cached = cached()
        return cached.ifEmpty { refresh() }
    }

    /**
     * Verify a typed PIN against the cached list.
     *
     * - Returns the matching PIN row when [plain] matches some active,
     *   unlocked entry's bcrypt hash.
     * - Returns null on no match. The caller decides whether that maps to
     *   a "wrong PIN" toast or a "locked" message; check [PinForDeviceItem.is_locked]
     *   on candidates if you need to disambiguate.
     *
     * bcrypt verification is intentionally O(n_pins) — at typical store
     * sizes (3-15 staff) that's a handful of ~250ms hashes which is fine
     * for a one-shot login flow but not something to call in a tight loop.
     */
    suspend fun verifyPin(plain: String): PinForDeviceItem? {
        if (plain.isBlank()) return null
        val pins = cached().ifEmpty { return null }
        val plainChars = plain.toCharArray()
        return pins.firstOrNull { pin ->
            pin.is_active && !pin.is_locked && verifyHash(plainChars, pin.pin_hash)
        }
    }

    private fun verifyHash(plain: CharArray, hash: String): Boolean = try {
        BCrypt.verifyer().verify(plain, hash.toCharArray()).verified
    } catch (_: IllegalArgumentException) {
        // Hash is malformed — log silently and treat as no match.
        false
    }
}
