package lu.rewardflow.terminal.ui.terminal

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
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.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.combine
import lu.rewardflow.terminal.data.api.LoyaltyApi
import lu.rewardflow.terminal.data.db.dao.PendingTransactionDao
import lu.rewardflow.terminal.data.model.CardLookupResponse
import lu.rewardflow.terminal.data.model.CategoryItem
import lu.rewardflow.terminal.data.model.EnrollRequest
import lu.rewardflow.terminal.data.model.PointsEarnRequest
import lu.rewardflow.terminal.data.model.PointsRedeemRequest
import lu.rewardflow.terminal.data.model.ProgramResponse
import lu.rewardflow.terminal.data.model.StampRedeemRequest
import lu.rewardflow.terminal.data.model.StampRequest
import lu.rewardflow.terminal.data.model.TransactionItem
import lu.rewardflow.terminal.data.network.NetworkMonitor
import lu.rewardflow.terminal.data.repository.CategoryRepository
import lu.rewardflow.terminal.data.repository.DeviceConfigRepository
import lu.rewardflow.terminal.data.repository.QueueRepository
import retrofit2.HttpException
import java.io.IOException
import javax.inject.Inject

/**
 * Powers the main POS terminal screen.
 *
 * Tracks: which customer is currently selected, the cached program +
 * category list, online state, and any open action sheet. Card-lookup
 * hits the API directly — no offline fallback because we need the
 * latest balance to render something accurate; queueing a "lookup"
 * doesn't make sense.
 *
 * Phase D.1: search + display.
 * Phase D.2: action sheets — stamp / earn points / redeem stamps /
 *            redeem reward, all online-only for now (Phase E adds the
 *            Room queue).
 */
@HiltViewModel
class TerminalViewModel @Inject constructor(
    private val api: LoyaltyApi,
    private val configRepository: DeviceConfigRepository,
    private val categoryRepository: CategoryRepository,
    private val queueRepository: QueueRepository,
    pendingTransactionDao: PendingTransactionDao,
    networkMonitor: NetworkMonitor,
    moshi: Moshi,
) : ViewModel() {

    /** Set by [TerminalScreen] from the route arguments so queued
     *  transactions can record which staff initiated the offline action. */
    private var currentStaffPinId: Int = 0

    fun setStaffPinId(id: Int) {
        currentStaffPinId = id
    }

    private val programAdapter: JsonAdapter<ProgramResponse> =
        moshi.adapter(ProgramResponse::class.java)

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

    init {
        loadCachedProgram()
        loadCategories()
        refreshRecentTransactions()
        // Kick the sync worker on every screen entry so anything queued in
        // a previous session drains as soon as the network constraint is
        // satisfied. Idempotent — KEEP policy in scheduleSync().
        queueRepository.scheduleSync()

        combine(
            networkMonitor.isOnline,
            pendingTransactionDao.getPendingCount(),
        ) { online, pending -> online to pending }
            .onEach { (online, pending) ->
                _state.value = _state.value.copy(
                    isOnline = online,
                    pendingSyncCount = pending,
                )
                // When the count drops to 0 after being non-zero, the
                // queue just drained; refresh the customer + feed so the
                // UI reflects the synced changes.
                if (pending == 0) refreshRecentTransactions()
            }
            .launchIn(viewModelScope)
    }

    fun onSearchChanged(query: String) {
        _state.value = _state.value.copy(searchQuery = query, errorMessage = null)
    }

    fun onSearchSubmit() {
        val q = _state.value.searchQuery.trim()
        if (q.isBlank()) return
        _state.value = _state.value.copy(
            isSearching = true,
            customer = null,
            errorMessage = null,
        )
        viewModelScope.launch {
            val result = runCatching { api.lookupCard(q) }
            _state.value = result.fold(
                onSuccess = { card ->
                    _state.value.copy(
                        isSearching = false,
                        customer = card,
                        searchQuery = "",
                    )
                },
                onFailure = { err ->
                    _state.value.copy(
                        isSearching = false,
                        errorMessage = err.message ?: "Lookup failed",
                    )
                },
            )
        }
    }

    fun lookupByCardNumber(cardNumber: String) {
        if (cardNumber.isBlank()) return
        onSearchChanged(cardNumber)
        onSearchSubmit()
    }

    // ── Scanner overlay ──────────────────────────────────────────────

    fun openScanner() {
        _state.value = _state.value.copy(scannerOpen = true, errorMessage = null)
    }

    fun dismissScanner() {
        _state.value = _state.value.copy(scannerOpen = false)
    }

    /** Decoded raw value from the QR scanner overlay. Loyalty card QRs
     *  encode the card_number, so we hand it straight to the lookup. */
    fun onCardQrScanned(rawValue: String) {
        _state.value = _state.value.copy(scannerOpen = false)
        lookupByCardNumber(rawValue)
    }

    // ── Enrollment dialog ────────────────────────────────────────────

    fun openEnrollDialog() {
        _state.value = _state.value.copy(
            enrollDialogOpen = true,
            enrolling = false,
            enrollError = null,
        )
    }

    fun dismissEnrollDialog() {
        _state.value = _state.value.copy(
            enrollDialogOpen = false,
            enrolling = false,
            enrollError = null,
        )
    }

    /** Submit the enrollment form. ``birthday`` is expected as an ISO
     *  ``YYYY-MM-DD`` string or null.
     *
     *  Online: enrolls + re-fetches the lookup-shape card so the
     *  customer pane renders fully.
     *
     *  Offline (IOException): queues the enrollment for later sync;
     *  the dialog closes and the cashier sees a "queued" toast. The
     *  customer pane is NOT pre-populated since we don't know the
     *  card_number until the server assigns one. */
    fun submitEnroll(
        name: String,
        email: String,
        phone: String?,
        birthday: String?,
    ) {
        if (_state.value.enrolling) return
        if (name.isBlank() || email.isBlank()) {
            _state.value = _state.value.copy(enrollError = "Name and email are required")
            return
        }
        val request = EnrollRequest(
            email = email.trim(),
            customer_name = name.trim(),
            customer_phone = phone?.trim()?.takeIf { it.isNotBlank() },
            customer_birthday = birthday?.trim()?.takeIf { it.isNotBlank() },
        )
        _state.value = _state.value.copy(enrolling = true, enrollError = null)
        viewModelScope.launch {
            val result = runCatching { api.enrollCustomer(request) }
            result.fold(
                onSuccess = { card ->
                    runCatching { api.lookupCard(card.card_number) }
                        .onSuccess { hydrated ->
                            _state.value = _state.value.copy(
                                enrolling = false,
                                enrollDialogOpen = false,
                                customer = hydrated,
                            )
                            refreshRecentTransactions()
                        }
                        .onFailure {
                            _state.value = _state.value.copy(
                                enrolling = false,
                                enrollDialogOpen = false,
                                searchQuery = card.card_number,
                            )
                        }
                },
                onFailure = { err ->
                    if (err is IOException) {
                        runCatching { queueRepository.queueEnroll(request, currentStaffPinId) }
                            .onSuccess {
                                _state.value = _state.value.copy(
                                    enrolling = false,
                                    enrollDialogOpen = false,
                                    actionResult = ActionResult.Queued,
                                )
                            }
                            .onFailure { qErr ->
                                _state.value = _state.value.copy(
                                    enrolling = false,
                                    enrollError = qErr.message ?: "Could not queue enrollment",
                                )
                            }
                    } else {
                        _state.value = _state.value.copy(
                            enrolling = false,
                            enrollError = readableErrorMessage(err),
                        )
                    }
                },
            )
        }
    }

    fun clearCustomer() {
        _state.value = _state.value.copy(
            customer = null,
            searchQuery = "",
            errorMessage = null,
        )
    }

    fun consumeError() {
        _state.value = _state.value.copy(errorMessage = null)
    }

    // ── Action sheets ────────────────────────────────────────────────

    fun openAction(kind: ActionKind) {
        if (_state.value.customer == null) return
        _state.value = _state.value.copy(
            activeAction = kind,
            actionResult = null,
            errorMessage = null,
        )
    }

    fun dismissAction() {
        _state.value = _state.value.copy(
            activeAction = null,
            actionInProgress = false,
            actionResult = null,
        )
    }

    /** Add a stamp to the currently-selected card. Queueable offline. */
    fun submitStamp(categoryIds: List<Int>) {
        val card = _state.value.customer ?: return
        val request = StampRequest(
            card_id = card.id,
            category_ids = categoryIds.ifEmpty { null },
        )
        runAction(
            block = {
                val response = api.addStamp(request)
                if (!response.success) error("Server reported failure")
            },
            queueOnNetworkFailure = { queueRepository.queueStamp(request, currentStaffPinId) },
        )
    }

    /** Earn points on a purchase. [amountCents] must be > 0. Queueable offline. */
    fun submitEarnPoints(amountCents: Int, categoryIds: List<Int>) {
        if (amountCents <= 0) {
            _state.value = _state.value.copy(
                actionResult = ActionResult.Failure("Amount must be greater than 0")
            )
            return
        }
        val card = _state.value.customer ?: return
        val request = PointsEarnRequest(
            card_id = card.id,
            purchase_amount_cents = amountCents,
            category_ids = categoryIds.ifEmpty { null },
        )
        runAction(
            block = {
                val response = api.earnPoints(request)
                if (!response.success) error("Server reported failure")
            },
            queueOnNetworkFailure = { queueRepository.queuePointsEarn(request, currentStaffPinId) },
        )
    }

    /** Redeem the stamp reward on the currently-selected card. Online-only. */
    fun submitRedeemStamps() = runAction(
        block = {
            val card = _state.value.customer ?: return@runAction
            val response = api.redeemStamps(StampRedeemRequest(card_id = card.id))
            if (!response.success) error("Server reported failure")
        },
        queueOnNetworkFailure = null,
    )

    /** Redeem a points reward by id. Online-only. */
    fun submitRedeemReward(rewardId: String) = runAction(
        block = {
            val card = _state.value.customer ?: return@runAction
            val response = api.redeemPoints(
                PointsRedeemRequest(card_id = card.id, reward_id = rewardId)
            )
            if (!response.success) error("Server reported failure")
        },
        queueOnNetworkFailure = null,
    )

    /**
     * Run [block] online. On [IOException] (lost network mid-call) and
     * with a non-null [queueOnNetworkFailure], the action is queued via
     * [QueueRepository] and the dialog reports queued-success. On
     * [HttpException] / other failures the dialog surfaces the server's
     * message — those are real errors, not connectivity blips.
     */
    private fun runAction(
        block: suspend () -> Unit,
        queueOnNetworkFailure: (suspend () -> Unit)?,
    ) {
        if (_state.value.actionInProgress) return
        _state.value = _state.value.copy(
            actionInProgress = true,
            actionResult = null,
        )
        viewModelScope.launch {
            val result = runCatching { block() }
            if (result.isSuccess) {
                _state.value = _state.value.copy(
                    actionInProgress = false,
                    actionResult = ActionResult.Success,
                    activeAction = null,
                )
                refreshCurrentCustomer()
                refreshRecentTransactions()
                return@launch
            }

            val err = result.exceptionOrNull()
            if (err is IOException && queueOnNetworkFailure != null) {
                runCatching { queueOnNetworkFailure() }
                    .onSuccess {
                        _state.value = _state.value.copy(
                            actionInProgress = false,
                            actionResult = ActionResult.Queued,
                            activeAction = null,
                        )
                    }
                    .onFailure { qErr ->
                        _state.value = _state.value.copy(
                            actionInProgress = false,
                            actionResult = ActionResult.Failure(
                                qErr.message ?: "Could not queue offline"
                            ),
                        )
                    }
                return@launch
            }

            _state.value = _state.value.copy(
                actionInProgress = false,
                actionResult = ActionResult.Failure(readableErrorMessage(err)),
            )
        }
    }

    /** Extract a human-readable message from a Retrofit/HttpException.
     *
     * Retrofit's default `message` is the HTTP status text ("Bad Request"),
     * which hides the JSON ``{message, error_code, details}`` body the
     * backend returns. Read the body and surface its message instead so
     * cashiers see "Staff PIN is required" or "Card not found" rather
     * than the generic 400. */
    private fun readableErrorMessage(t: Throwable?): String {
        if (t is HttpException) {
            val body = runCatching { t.response()?.errorBody()?.string() }.getOrNull()
            if (!body.isNullOrBlank()) {
                // Cheap JSON peek — avoid pulling in a full adapter for one field.
                val match = "\"message\"\\s*:\\s*\"([^\"]+)\"".toRegex().find(body)
                if (match != null) return match.groupValues[1]
                return body.take(200)
            }
            return "HTTP ${t.code()}"
        }
        return t?.message ?: "Operation failed"
    }

    fun refreshCurrentCustomer() {
        val current = _state.value.customer ?: return
        viewModelScope.launch {
            runCatching { api.lookupCard(current.card_number) }
                .onSuccess { card -> _state.value = _state.value.copy(customer = card) }
        }
    }

    /** Pull the latest ~10 transactions for the left-pane feed. Failure is
     *  silent: a missing list is acceptable degradation, the rest of the
     *  terminal screen still works. */
    fun refreshRecentTransactions() {
        viewModelScope.launch {
            runCatching { api.listRecentTransactions(skip = 0, limit = 10) }
                .onSuccess { resp ->
                    _state.value = _state.value.copy(
                        recentTransactions = resp.transactions,
                    )
                }
        }
    }

    // ── Internal ─────────────────────────────────────────────────────

    private fun loadCachedProgram() {
        viewModelScope.launch {
            val json = configRepository.programJson.first()
            if (json != null) {
                val program = runCatching { programAdapter.fromJson(json) }.getOrNull()
                if (program != null) {
                    _state.value = _state.value.copy(program = program)
                }
            }
        }
    }

    private fun loadCategories() {
        viewModelScope.launch {
            // Cached list first so the action sheets render instantly;
            // refresh in the background to pick up new categories.
            val cached = runCatching { categoryRepository.listOrRefresh() }
                .getOrDefault(emptyList())
            _state.value = _state.value.copy(categories = cached)
        }
    }
}

data class TerminalUiState(
    val program: ProgramResponse? = null,
    val categories: List<CategoryItem> = emptyList(),
    val searchQuery: String = "",
    val isSearching: Boolean = false,
    val customer: CardLookupResponse? = null,
    val errorMessage: String? = null,
    val isOnline: Boolean = true,
    val activeAction: ActionKind? = null,
    val actionInProgress: Boolean = false,
    val actionResult: ActionResult? = null,
    val recentTransactions: List<TransactionItem> = emptyList(),
    val scannerOpen: Boolean = false,
    val enrollDialogOpen: Boolean = false,
    val enrolling: Boolean = false,
    val enrollError: String? = null,
    val pendingSyncCount: Int = 0,
)

enum class ActionKind { AddStamp, EarnPoints, RedeemStamps, RedeemReward }

sealed interface ActionResult {
    data object Success : ActionResult

    /** The action couldn't reach the server but was queued in Room and
     *  will sync when connectivity returns. The cashier sees a "queued"
     *  message instead of a hard failure. */
    data object Queued : ActionResult
    data class Failure(val message: String) : ActionResult
}
