package wang.yaojia.webterm.viewmodels

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import wang.yaojia.webterm.api.pairing.PairingError
import wang.yaojia.webterm.api.pairing.PairingProbeResult
import wang.yaojia.webterm.api.pairing.runPairingProbe
import wang.yaojia.webterm.hostregistry.Host
import wang.yaojia.webterm.hostregistry.HostStore
import wang.yaojia.webterm.wire.HostClassifier
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.HostNetworkTier
import wang.yaojia.webterm.wire.HttpTransport
import wang.yaojia.webterm.wire.TermTransport
import java.util.UUID

/**
 * # PairingViewModel (A19) — QR/manual URL → confirm-before-network → two-step probe → secure save.
 *
 * The pairing state machine, ported 1:1 from the iOS pairing flow, as a pure JVM presenter (mirrors
 * [GateViewModel] — a plain class with [bind], NOT an `androidx.lifecycle.ViewModel`, so it runs under
 * `runTest` virtual time with no `Dispatchers.Main`/Robolectric). The Compose/CameraX/ML-Kit/permission
 * shell ([wang.yaojia.webterm.screens.PairingScreen]) is device-QA (plan §7); THIS is the JVM-tested core.
 *
 * ### The four hard rules this class enforces (plan §5.4 / §8)
 *  1. **One validator.** Every scanned or typed string funnels through [HostEndpoint.fromBaseUrl] — the
 *     single frozen Origin/WS validator (:wire-protocol). A string it rejects never reaches the network.
 *  2. **Confirm-before-network.** [onManualEntry]/[onQrScanned] only VALIDATE + classify; they never
 *     probe. The network is touched exclusively by [confirm]/[retry]. A scanned URL is never
 *     auto-connected.
 *  3. **Public host needs an explicit acknowledge.** A [WarningTier.PUBLIC] host cannot be probed until
 *     the user ticks the risk acknowledge ([confirm]`(acknowledgedPublicRisk = true)`); otherwise the
 *     confirm is a no-op that re-arms the reminder.
 *  4. **Tunnel host is cert-gated (the choke point retry can't bypass).** Both [confirm] and [retry]
 *     funnel through the ONE private [runProbe]; for a [WarningTier.TUNNEL] host with no installed
 *     device cert, [runProbe] refuses BEFORE any network I/O and re-refuses on every retry (plan §8
 *     "runProbe refuses to probe `*.terminal.yaojia.wang` unless a device cert is installed").
 *
 * On probe success the validated host is persisted via [HostStore.upsert].
 *
 * @param hostStore where a successfully-paired [Host] is saved.
 * @param prober the two-step [wang.yaojia.webterm.api.pairing.runPairingProbe] seam; a fake in tests.
 * @param hasDeviceCert `suspend` cert check (production: `IdentityRepository.hasInstalledIdentity` on IO).
 * @param newId host-id allocator (default random UUID; deterministic in tests).
 */
public class PairingViewModel(
    private val hostStore: HostStore,
    private val prober: PairingProber,
    private val hasDeviceCert: suspend () -> Boolean,
    private val newId: () -> String = { UUID.randomUUID().toString() },
) {
    private val _uiState = MutableStateFlow<PairingUiState>(PairingUiState.Entry())

    /** The single snapshot the pairing UI renders from. */
    public val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()

    private var scope: CoroutineScope? = null
    private var probeJob: Job? = null

    /** Bind the scope probes launch into (the screen passes a lifecycle scope). Idempotent-ish: last wins. */
    public fun bind(scope: CoroutineScope) {
        this.scope = scope
    }

    // ── Input funnels (RULE 1 + RULE 2: validate + classify only, NEVER probe) ──────────────────────

    /** Manual URL entry submit. Validates through the one validator; on success shows the confirm gate. */
    public fun onManualEntry(rawUrl: String) {
        presentCandidate(rawUrl)
    }

    /**
     * A decoded QR payload. IDENTICAL funnel to [onManualEntry] — a scanned string is untrusted external
     * input and is NEVER auto-connected (RULE 2); it only advances to the confirm gate.
     */
    public fun onQrScanned(rawUrl: String) {
        presentCandidate(rawUrl)
    }

    private fun presentCandidate(rawUrl: String) {
        val endpoint = HostEndpoint.fromBaseUrl(rawUrl)
        if (endpoint == null) {
            _uiState.value = PairingUiState.Entry(error = EntryError.INVALID_URL)
            return
        }
        _uiState.value = PairingUiState.Confirming(
            endpoint = endpoint,
            name = defaultName(endpoint),
            tier = PairingTiers.tierFor(endpoint),
        )
    }

    /** Edit the host display name on the confirm screen (no effect off the confirm state). */
    public fun setName(name: String) {
        val state = _uiState.value
        if (state is PairingUiState.Confirming) _uiState.value = state.copy(name = name)
    }

    /** Discard the current candidate/result and return to the entry field. */
    public fun reset() {
        probeJob?.cancel()
        _uiState.value = PairingUiState.Entry()
    }

    // ── The network choke point (RULE 3 + RULE 4) ──────────────────────────────────────────────────

    /**
     * Confirm the on-screen candidate and probe it. For a public host, [acknowledgedPublicRisk] MUST be
     * true or this re-arms the acknowledge reminder without touching the network (RULE 3).
     */
    public fun confirm(acknowledgedPublicRisk: Boolean = false) {
        val candidate = _uiState.value.candidate() ?: return
        runProbe(candidate, acknowledgedPublicRisk)
    }

    /**
     * Retry after a failure or a cert-gate refusal. Funnels through the SAME [runProbe] choke point as
     * [confirm] — so the tunnel cert-gate (RULE 4) cannot be bypassed by retrying.
     */
    public fun retry(acknowledgedPublicRisk: Boolean = false) {
        val candidate = _uiState.value.candidate() ?: return
        runProbe(candidate, acknowledgedPublicRisk)
    }

    private fun runProbe(candidate: Candidate, acknowledgedPublicRisk: Boolean) {
        val tier = candidate.tier
        // RULE 3 — public host: no probe until the risk is explicitly acknowledged.
        if (tier.needsExplicitAck && !acknowledgedPublicRisk) {
            _uiState.value = PairingUiState.Confirming(
                endpoint = candidate.endpoint,
                name = candidate.name,
                tier = tier,
                awaitingAck = true,
            )
            return
        }
        val scope = scope ?: return
        probeJob?.cancel()
        probeJob = scope.launch {
            // RULE 4 — tunnel cert-gate: refuse BEFORE any network I/O; retry re-hits this same guard.
            if (tier.isCertGated && !hasDeviceCert()) {
                _uiState.value = PairingUiState.CertRequired(candidate.endpoint, candidate.name, tier)
                return@launch
            }
            _uiState.value = PairingUiState.Probing(candidate.endpoint, candidate.name, tier)
            when (val result = prober.probe(candidate.endpoint)) {
                is PairingProbeResult.Success -> onProbeSuccess(candidate)
                is PairingProbeResult.Failure -> _uiState.value = PairingUiState.Failed(
                    endpoint = candidate.endpoint,
                    name = candidate.name,
                    tier = tier,
                    error = result.error,
                    message = PairingCopy.describe(result.error, tier),
                )
            }
        }
    }

    private suspend fun onProbeSuccess(candidate: Candidate) {
        val host = Host.create(id = newId(), name = candidate.name.ifBlank { defaultName(candidate.endpoint) }, baseUrl = candidate.endpoint.baseUrl)
        if (host == null) {
            // Unreachable in practice (the endpoint already validated), but never persist a null host.
            _uiState.value = PairingUiState.Failed(
                endpoint = candidate.endpoint,
                name = candidate.name,
                tier = candidate.tier,
                error = PairingError.HttpOkButNotWebTerminal,
                message = PairingCopy.describe(PairingError.HttpOkButNotWebTerminal, candidate.tier),
            )
            return
        }
        hostStore.upsert(host)
        _uiState.value = PairingUiState.Paired(host)
    }

    private fun defaultName(endpoint: HostEndpoint): String =
        HostClassifier.hostOf(endpoint).ifEmpty { endpoint.baseUrl }
}

/** The two-step pairing probe seam ([wang.yaojia.webterm.api.pairing.runPairingProbe]); faked in tests. */
public fun interface PairingProber {
    public suspend fun probe(endpoint: HostEndpoint): PairingProbeResult
}

/**
 * Production [PairingProber] binding: the frozen two-step [runPairingProbe] over the ONE shared
 * `OkHttpClient`'s [HttpTransport] + [TermTransport] (injected in `NetworkModule`). The screen's nav
 * layer resolves both transports off the DI graph and hands the resulting prober to the ViewModel —
 * `warmUp()` must have run first (off-`Main`) so the first handshake here doesn't do AndroidKeyStore/Tink
 * I/O on a UI thread.
 */
public fun pairingProber(http: HttpTransport, ws: TermTransport): PairingProber =
    PairingProber { endpoint -> runPairingProbe(endpoint, http, ws) }

// ── Warning tiers (plan §5.4) ───────────────────────────────────────────────────────────────────────

/**
 * The pairing warning tiers the UI reproduces (plan §5.4 / §8). The four [HostNetworkTier] network
 * classes plus [TUNNEL] — the native tunnel domain (`*.terminal.yaojia.wang`), which classifies as
 * [HostNetworkTier.PUBLIC] on the wire but is cert-gated rather than ack-gated (its warning is softened
 * because the device cert IS the gate).
 */
public enum class WarningTier {
    /** localhost / 127.0.0.0/8 / ::1 — no warning. */
    LOOPBACK,

    /** RFC1918 / link-local / mDNS — non-blocking `ws://` cleartext notice. */
    PRIVATE_LAN,

    /** Tailscale CGNAT / MagicDNS — WireGuard-encrypted; no cleartext warning. */
    TAILSCALE,

    /** Native tunnel `*.terminal.yaojia.wang` — cert-gated; warning softened, TLS failure = cert invalid. */
    TUNNEL,

    /** Everything else (incl. malformed — fail-safe) — strongest blocking warning, explicit ack required. */
    PUBLIC,
}

/** Pure tier classification + gate policy (independently unit-tested; plan §5.4). */
public object PairingTiers {
    /** The native-tunnel host suffix — a subdomain of this presents a real LE cert but is mTLS-gated. */
    private const val TUNNEL_SUFFIX: String = ".terminal.yaojia.wang"

    /**
     * Classify [endpoint] into a [WarningTier]. The tunnel domain is detected FIRST (it would otherwise
     * fall to [WarningTier.PUBLIC]); everything else defers to the frozen [HostClassifier]. Fail-safe:
     * an unclassifiable host is [WarningTier.PUBLIC].
     */
    public fun tierFor(endpoint: HostEndpoint): WarningTier {
        val host = HostClassifier.hostOf(endpoint).lowercase()
        if (host.endsWith(TUNNEL_SUFFIX)) return WarningTier.TUNNEL
        return when (HostClassifier.classify(endpoint)) {
            HostNetworkTier.LOOPBACK -> WarningTier.LOOPBACK
            HostNetworkTier.PRIVATE_LAN -> WarningTier.PRIVATE_LAN
            HostNetworkTier.TAILSCALE -> WarningTier.TAILSCALE
            HostNetworkTier.PUBLIC -> WarningTier.PUBLIC
        }
    }
}

/** Only a public host demands the explicit risk acknowledge before probing (RULE 3). */
public val WarningTier.needsExplicitAck: Boolean get() = this == WarningTier.PUBLIC

/** Only the native-tunnel host is device-cert-gated (RULE 4). */
public val WarningTier.isCertGated: Boolean get() = this == WarningTier.TUNNEL

// ── UI state ────────────────────────────────────────────────────────────────────────────────────────

/** The pairing screen snapshot. */
public sealed interface PairingUiState {
    /** The URL/QR entry field, optionally showing a validation [error]. */
    public data class Entry(val error: EntryError? = null) : PairingUiState

    /**
     * A validated candidate awaiting the user's explicit confirm (RULE 2). [awaitingAck] is set after a
     * public-host confirm without acknowledgement (RULE 3) so the UI can highlight the risk toggle.
     */
    public data class Confirming(
        val endpoint: HostEndpoint,
        val name: String,
        val tier: WarningTier,
        val awaitingAck: Boolean = false,
    ) : PairingUiState

    /** The two-step probe is running against [endpoint]. */
    public data class Probing(val endpoint: HostEndpoint, val name: String, val tier: WarningTier) : PairingUiState

    /**
     * A tunnel host was refused for lack of a device cert (RULE 4). The user must import a cert (A27)
     * then [PairingViewModel.retry] — which re-hits the same gate.
     */
    public data class CertRequired(val endpoint: HostEndpoint, val name: String, val tier: WarningTier) : PairingUiState

    /** The probe failed; [message] is inert, app-authored copy for [error]. Retryable. */
    public data class Failed(
        val endpoint: HostEndpoint,
        val name: String,
        val tier: WarningTier,
        val error: PairingError,
        val message: String,
    ) : PairingUiState

    /** Paired + persisted. The screen navigates onward from here. */
    public data class Paired(val host: Host) : PairingUiState
}

/** Client-side validation failures on the entry field. */
public enum class EntryError {
    /** The typed/scanned string is not a valid `http(s)` URL with a host. */
    INVALID_URL,
}

/** The endpoint+name+tier a probe runs against, recovered from whatever state currently holds one. */
private data class Candidate(val endpoint: HostEndpoint, val name: String, val tier: WarningTier)

private fun PairingUiState.candidate(): Candidate? = when (this) {
    is PairingUiState.Confirming -> Candidate(endpoint, name, tier)
    is PairingUiState.Probing -> Candidate(endpoint, name, tier)
    is PairingUiState.CertRequired -> Candidate(endpoint, name, tier)
    is PairingUiState.Failed -> Candidate(endpoint, name, tier)
    is PairingUiState.Entry, is PairingUiState.Paired -> null
}

/** Maps the [PairingError] taxonomy to inert, app-authored Chinese copy (never a server string, §8). */
internal object PairingCopy {
    fun describe(error: PairingError, tier: WarningTier): String = when (error) {
        is PairingError.HostUnreachable ->
            "无法连接到主机。请检查地址、端口，以及设备是否在同一局域网 / Tailscale 网络内。"
        PairingError.HttpOkButNotWebTerminal ->
            "这个端口在运行别的服务，不是 web-terminal。端口对吗？"
        is PairingError.OriginRejected -> error.hint // already actionable, endpoint-derived
        is PairingError.CleartextBlocked ->
            "系统阻止了到 ${error.host} 的明文连接。仅局域网 / Tailscale 地址允许 ws:// 明文。"
        PairingError.TlsFailure ->
            // Tunnel hosts present a real LE server cert, so a TLS failure there means OUR client cert
            // is the problem (plan §5.4: "TLS failure re-mapped to client cert invalid/revoked").
            if (tier == WarningTier.TUNNEL) "客户端证书无效或已被吊销。请在“设备证书”里重新导入后再试。"
            else "TLS 握手失败：无法验证服务器证书。"
        PairingError.Timeout ->
            "连接超时。主机可能不可达，或响应过慢。"
    }
}
