package wang.yaojia.webterm.push

import wang.yaojia.webterm.api.models.HookDecision
import wang.yaojia.webterm.api.routes.ApiClient
import wang.yaojia.webterm.api.routes.ApiClientError
import wang.yaojia.webterm.hostregistry.HostStore
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wiring.ApiClientFactory
import java.util.UUID

/**
 * The single, shared decision-POST path used by BOTH the Allow trampoline (after biometric auth)
 * and the Deny receiver. Extracted so the token discipline lives in exactly one place (DRY) and is
 * JVM-unit-testable behind seams — no `FirebaseMessagingService`/`BiometricPrompt` needed.
 *
 * ### Which host?
 * The push payload carries NO host (content minimization, plan §4.5). The same FCM registration is
 * shared across every paired host (A31 registers the one device token per host), so on arrival we
 * cannot know which host issued the token. We therefore try each paired host in turn and stop at the
 * first that ACCEPTS (`204`). A host that did not issue this single-use token replies `403`
 * ([ApiClientError.DecisionRejected]) and we move on — safe because the token is host-scoped and
 * single-use, so a foreign host cannot act on it.
 *
 * ### Token discipline (plan §8, non-negotiable)
 * [token] is passed ONLY to [ApiClient.hookDecision]. It is NEVER persisted and NEVER logged: the
 * [diagnostics] seam receives outcomes and error *types* only, never the token (nor sessionId). A
 * retried-after-success POST returns `403` (server idempotency), which this treats as "not accepted"
 * and never crashes on.
 */
public class PushDecisionSubmitter(
    private val loadEndpoints: suspend () -> List<HostEndpoint>,
    private val clientFor: (HostEndpoint) -> ApiClient,
    private val diagnostics: (String) -> Unit = {},
) {
    /**
     * POST `{ sessionId, decision, token }` to the issuing host. Returns true iff a host accepted.
     * Never throws — every failure (no host, transport error, 403/429) is swallowed to a `false`
     * so the fail-safe Deny path and the fire-and-forget Allow path stay robust.
     */
    public suspend fun submit(sessionId: UUID, decision: HookDecision, token: String): Boolean {
        val endpoints = runCatching { loadEndpoints() }.getOrElse {
            diagnostics("decision(${decision.wire}): host lookup failed (${it.javaClass.simpleName})")
            return false
        }
        if (endpoints.isEmpty()) {
            diagnostics("decision(${decision.wire}): no paired host to post to")
            return false
        }
        for (endpoint in endpoints) {
            if (postToOne(endpoint, sessionId, decision, token)) return true
        }
        diagnostics("decision(${decision.wire}): no host accepted")
        return false
    }

    /** One host attempt. Returns true only on `204`; classifies every other outcome without the token. */
    private suspend fun postToOne(
        endpoint: HostEndpoint,
        sessionId: UUID,
        decision: HookDecision,
        token: String,
    ): Boolean =
        try {
            clientFor(endpoint).hookDecision(sessionId, decision, token)
            diagnostics("decision(${decision.wire}): accepted")
            true
        } catch (_: ApiClientError.DecisionRejected) {
            // Wrong host, or a stale/used token (idempotency). Not this host — try the next.
            diagnostics("decision(${decision.wire}): rejected by a host")
            false
        } catch (_: ApiClientError.RateLimited) {
            diagnostics("decision(${decision.wire}): rate-limited")
            false
        } catch (e: Exception) {
            // Transport / unexpected status. Best-effort: log the TYPE only, move on.
            diagnostics("decision(${decision.wire}): error (${e.javaClass.simpleName})")
            false
        }

    public companion object {
        /** Wire the seams from the Hilt graph (used by the Deny receiver and Allow activity). */
        public fun fromRegistry(
            hostStore: HostStore,
            apiClientFactory: ApiClientFactory,
            diagnostics: (String) -> Unit = {},
        ): PushDecisionSubmitter =
            PushDecisionSubmitter(
                loadEndpoints = { hostStore.loadAll().map { it.endpoint } },
                clientFor = { endpoint -> apiClientFactory.create(endpoint) },
                diagnostics = diagnostics,
            )
    }
}
