package io.unom.punktfunk.kit

/**
 * The single JNI seam to `libpunktfunk_android.so` (the Rust-heavy client core).
 *
 * Symbols are implemented in `clients/android/native`. This object is intentionally thin —
 * all protocol logic lives in Rust (`punktfunk-core` + the connector); Kotlin only marshals.
 */
object NativeBridge {
    init {
        System.loadLibrary("punktfunk_android")
    }

    /** punktfunk-core C-ABI version. A successful call proves the native library is linked. */
    external fun abiVersion(): Int

    /** punktfunk-core crate version string. */
    external fun coreVersion(): String

    /**
     * Mint a fresh persistent self-signed identity, returned as
     * `"<certPem>\n-----PUNKTFUNK-KEY-----\n<keyPem>"`, or `""` on error. Kotlin persists it
     * (Keystore-wrapped via `IdentityStore`) and only calls this again when the store is empty.
     */
    external fun nativeGenerateIdentity(): String

    /**
     * Connect, presenting [certPem]/[keyPem] (both empty = anonymous) and pinning [pinHex] (empty =
     * trust-on-first-use — read [nativeHostFingerprint] after; else 64-hex host SHA-256, mismatch →
     * `0`). [width]/[height]/[refreshHz] are the requested virtual-output mode (the host streams at
     * exactly this); [bitrateKbps] 0 = host default; [compositorPref]/[gamepadPref] are the
     * `CompositorPref`/`GamepadPref` wire bytes (0 = Auto). [timeoutMs] is the handshake budget — the
     * normal path passes a short value, the no-PIN "request access" path a long one (≥ the host's
     * approval-park window) so a slow operator approval lands on this same parked connection. Returns
     * an opaque session handle, or `0` on failure. Pair with exactly one [nativeClose].
     */
    external fun nativeConnect(
        host: String,
        port: Int,
        width: Int,
        height: Int,
        refreshHz: Int,
        certPem: String,
        keyPem: String,
        pinHex: String,
        bitrateKbps: Int,
        compositorPref: Int,
        gamepadPref: Int,
        hdrEnabled: Boolean,
        audioChannels: Int,
        /** `quic::CODEC_*` bitfield of codecs this device decodes ([VideoDecoders.decodableCodecBits]);
         *  `0` falls back to H.264|HEVC. The host resolves the emitted codec from this ∩ its GPU. */
        videoCodecs: Int,
        /** Preferred video codec as a `quic::CODEC_*` bit (`0` = auto). Soft — the host falls back. */
        preferredCodec: Int,
        timeoutMs: Int,
        /** Store-qualified library id (`steam:<appid>` / `custom:<id>`) to boot straight into a game,
         *  or `null`/empty for a plain desktop connect. Rides the Hello as `launch`. */
        launch: String?,
    ): Long

    /** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
    external fun nativeHostFingerprint(handle: Long): String

    /**
     * Has the underlying QUIC session ended? `true` once the connection closed — a host suspend /
     * crash / network drop idle-timed it out (~8 s), or the host closed it — from then on no frame
     * ever arrives and the video sits frozen on its last one. The stream watchdog polls this (~1 Hz)
     * to leave a dead stream and return to the menu, where the user can Wake-on-LAN the host, instead
     * of stranding them on a frozen frame. `false` on a `0` handle. Cheap (one atomic load); UI-safe.
     */
    external fun nativeSessionEnded(handle: Long): Boolean

    /**
     * Run the SPAKE2 PIN ceremony, presenting [certPem]/[keyPem]. Returns the host's verified
     * fingerprint (64-hex) to persist + pin, or `""` on failure (wrong PIN / MITM / unreachable).
     * Blocking — call off the main thread.
     */
    external fun nativePair(
        host: String,
        port: Int,
        certPem: String,
        keyPem: String,
        pin: String,
        name: String,
    ): String

    /**
     * The machine token of the most recent failed [nativeConnect]/[nativePair], cleared on read
     * (`""` when none) — call right after a `0` handle / `""` fingerprint. A typed host rejection
     * yields its wire token ("not-armed", "denied", "approval-timeout", "superseded", "busy",
     * "rate-limited", "bound-other", "identity-required", "wire-version"); transport-level causes
     * yield "crypto" (wrong PIN / identity mismatch), "timeout", "io", or "error". Lets the UI say
     * WHY instead of the old catch-all that blamed the PIN for dead network paths.
     */
    external fun nativeTakeLastError(): String

    /**
     * Signal a **deliberate** user disconnect on [handle] before [nativeClose]: the session closes
     * with `QUIT_CLOSE_CODE` so the host tears it down immediately instead of holding the keep-alive
     * linger for a reconnect. Call from an explicit disconnect gesture only — NOT from a
     * host-ended/network-drop end or an app-background (those keep the linger). No-op on `0`.
     */
    external fun nativeDisconnectQuit(handle: Long)

    /** Tear down a session handle returned by [nativeConnect]. No-op on `0`. */
    external fun nativeClose(handle: Long)

    // ---- LAN discovery: mDNS browse of `_punktfunk._udp` in Rust (mdns-sd), polled by Kotlin ----
    // Replaces NsdManager. The caller holds the Wi-Fi MulticastLock for the browse lifetime; raw
    // multicast *reception* needs it. See io.unom.punktfunk.kit.discovery.HostDiscovery.

    /**
     * Start browsing `_punktfunk._udp` on the LAN. Returns an opaque discovery handle, or `0` on
     * failure. Pair with exactly one [nativeDiscoveryStop]. Cheap + non-blocking (spawns the mDNS
     * daemon + a fold thread).
     */
    external fun nativeDiscoveryStart(): Long

    /**
     * The current resolved-host snapshot for [handle]: newline-joined records, each
     * `key␟name␟addr␟port␟fp␟pair␟mac` (`␟` = U+001F). Empty string = no hosts / `0` handle. Poll ~1 Hz;
     * cheap (a lock + string build), safe to call on the main thread.
     */
    external fun nativeDiscoveryPoll(handle: Long): String

    /** Stop the browse, shut the mDNS daemon down and join its thread. No-op on `0`. */
    external fun nativeDiscoveryStop(handle: Long)

    /**
     * Send a Wake-on-LAN magic packet to wake a sleeping host. [macsCsv] is comma-separated MAC
     * addresses (`aa:bb:..,cc:dd:..`), learned from the host's mDNS `mac` TXT while it was online;
     * [lastIp] is the host's last-known IPv4 (or empty). Returns true if at least one datagram was
     * sent. No handle — callable without a live session. Do NOT call on the main thread (it does
     * blocking socket sends); run it on a background dispatcher.
     */
    external fun nativeWakeOnLan(macsCsv: String, lastIp: String): Boolean

    /**
     * Bounded, trust-agnostic QUIC reachability probe to [host]:[port] (mDNS-independent): true if
     * the host completed the handshake within [timeoutMs]. No pin/identity presented. Lets a saved
     * host reached over a routed network (Tailscale/VPN/another subnet) — which never advertises on
     * mDNS — still show as online. Blocking (builds its own runtime) — run on a background
     * dispatcher, never the main thread.
     */
    external fun nativeProbe(host: String, port: Int, timeoutMs: Int): Boolean

    /**
     * Apply the user's "Low-latency mode (experimental)" toggle to the process-wide transport
     * defaults — today just DSCP/QoS marking on the media sockets. Must be called BEFORE
     * [nativeConnect] (the tag is applied at socket creation); `HostConnect.connectToHost` does.
     * The rest of the toggle rides explicit per-session parameters ([nativeStartVideo] /
     * [nativeStartAudio]). Cheap (one atomic store); UI-safe.
     */
    external fun nativeSetLowLatencyMode(enabled: Boolean)

    /**
     * The MediaCodec MIME the host resolved for this session (`"video/hevc"` / `"video/avc"` /
     * `"video/av01"`), or `""` on a `0` handle. Kotlin ranks `MediaCodecList` decoders for this
     * MIME (see [io.unom.punktfunk.kit.VideoDecoders]) before [nativeStartVideo]. Cheap; UI-safe.
     */
    external fun nativeVideoMime(handle: Long): String

    /**
     * A short human label for the codec the host resolved (`"H.264"` / `"HEVC"` / `"AV1"` /
     * `"PyroWave"`), for the stats HUD's video-feed line, or `""` on a `0` handle. Distinct from
     * [nativeVideoMime] because the MIME collapses PyroWave onto `video/hevc` and can't name it.
     * Fixed for the session (resolved at the handshake); read once. Cheap; UI-safe.
     */
    external fun nativeVideoCodecLabel(handle: Long): String

    /**
     * Start the decode thread rendering onto [surface] (a SurfaceView's surface). Decode runs
     * entirely in Rust (NDK AMediaCodec → ANativeWindow) — no per-frame JNI. [decoderName] is the
     * decoder Kotlin ranked from `MediaCodecList` (`""` = let the platform resolve the default for
     * the MIME — what the pre-overhaul client always did); [lowLatencyMode] is the user's
     * "Low-latency mode (experimental)" toggle (off, the default, runs the original decode
     * pipeline; on, the aggressive per-SoC tuning + async loop); [lowLatencyFeature] is whether
     * [decoderName] advertised `FEATURE_LowLatency` (HUD label only). [isTv] drives an active HDMI
     * mode switch to the stream refresh on TV boxes when the toggle is on (vs. the softer seamless
     * hint otherwise). No-op if already started.
     */
    external fun nativeStartVideo(
        handle: Long,
        surface: android.view.Surface,
        decoderName: String,
        lowLatencyMode: Boolean,
        lowLatencyFeature: Boolean,
        isTv: Boolean,
    )

    /** Stop + join the decode thread without closing the session. No-op on `0`. */
    external fun nativeStopVideo(handle: Long)

    /**
     * The resolved decoder identity for the HUD, e.g. `c2.qti.avc.decoder · low-latency`, or `""`
     * before the decode thread has resolved one. One-shot (fixed for the session); poll once after
     * the HUD appears.
     */
    external fun nativeVideoDecoderLabel(handle: Long): String

    /**
     * Drain ~1 s of live decode stats for the on-stream HUD, or `null` when no decode thread runs.
     * Returns 26 doubles (unified stats spec, `design/stats-unification.md`):
     * `[fps, mbps, e2eP50Ms, e2eP95Ms, latValid, skewCorrected, width, height, refreshHz, framesLost,
     * bitDepth, colorPrimaries, colorTransfer, chromaFormatIdc, hostNetP50Ms, decodeP50Ms, hostP50Ms,
     * netP50Ms, lostWindow, skippedWindow, fecWindow, framesWindow, dispValid, displayP50Ms,
     * e2eDispP50Ms, e2eDispP95Ms]`
     * (the flags are 1.0/0.0; indexes 2/3 are the end-to-end capture→decoded headline; 10–13
     * describe the negotiated video feed — bit depth 8/10, CICP primaries/transfer, and the HEVC
     * chroma_format_idc 1=4:2:0 / 3=4:4:4; 14/15 are the stage p50s tiling the headline —
     * `host+network` = capture→received, `decode` = received→decoded; 16/17 split the
     * `host+network` term via the host's per-AU 0xCF timings — `host` = the host's capture→sent,
     * `network` = the remainder — both 0.0 when no timing matched this window, i.e. an old host;
     * 18–21 are the per-window reliability counters — lost/skipped/FEC/received; 22–25 are the
     * `display` stage from the OnFrameRendered render timestamps — when `dispValid` is 1.0 the
     * headline becomes the directly-measured capture→displayed pair at 24/25, tiled by
     * `host+network` + `decode` + `display` (23), and when 0.0 the HUD falls back to the
     * capture→decoded headline at 2/3 without the `display` term).
     * Poll ~1 Hz; each call resets the measurement window.
     */
    external fun nativeVideoStats(handle: Long): DoubleArray?

    /**
     * Gate per-frame stats sampling on the HUD being visible: while disabled the decode thread
     * skips the per-AU clock read + lock, so toggle this with the overlay (and only poll
     * [nativeVideoStats] while it's on). Enabling resets the measurement window — no stale data.
     * Sticky for the session (survives video stop/start). No-op on `0`.
     */
    external fun nativeSetVideoStatsEnabled(handle: Long, enabled: Boolean)

    /**
     * Start host→client audio: Opus decode → jitter ring → AAudio (LowLatency), all in Rust.
     * [lowLatencyMode] (the experimental toggle) additionally tags the stream usage=Game for the
     * HAL's game-audio routing. No-op if already started. Best-effort — a failure leaves video
     * streaming.
     */
    external fun nativeStartAudio(handle: Long, lowLatencyMode: Boolean)

    /** Stop + join the audio thread and close AAudio, without closing the session. No-op on `0`. */
    external fun nativeStopAudio(handle: Long)

    /**
     * Start mic uplink: AAudio input → Opus (48 kHz stereo, 20 ms) → host (`send_mic` / 0xCB), all in
     * Rust. No-op if already running. The caller MUST hold RECORD_AUDIO; otherwise the AAudio input
     * stream fails to open and the rest of the session keeps streaming.
     */
    external fun nativeStartMic(handle: Long)

    /** Stop + join the mic thread and close the AAudio input stream. No-op on `0`. */
    external fun nativeStopMic(handle: Long)

    // ---- Input: Kotlin captures, Rust forwards to the host (send_input) ----

    /** Relative mouse move; dx/dy are device-pixel deltas (screen +y down). */
    external fun nativeSendPointerMove(handle: Long, dx: Int, dy: Int)

    /**
     * Absolute mouse position — the host moves the cursor to (x, y) in a [surfaceWidth]×[surfaceHeight]
     * pixel space (it normalizes against that size and maps into the output region). Touch
     * "direct pointing": the cursor jumps to the finger. Parity with the Apple client's absolute touch.
     */
    external fun nativeSendPointerAbs(handle: Long, x: Int, y: Int, surfaceWidth: Int, surfaceHeight: Int)

    /** One mouse-button transition. button: 1=left 2=middle 3=right 4=X1 5=X2. */
    external fun nativeSendPointerButton(handle: Long, button: Int, down: Boolean)

    /** One scroll step. axis: 0=vertical 1=horizontal. delta: signed, 120-scaled, +=up/right. */
    external fun nativeSendScroll(handle: Long, axis: Int, delta: Int)

    /**
     * One REAL touchscreen transition (the touch-passthrough input mode). [kind]: 0=down 1=move
     * 2=up. [id] distinguishes fingers and is reusable after up; coordinates are pixels on the
     * client's touch surface — the host rescales against [surfaceWidth]×[surfaceHeight] and
     * injects a real touch contact. On up only [id] matters.
     */
    external fun nativeSendTouch(
        handle: Long,
        id: Int,
        kind: Int,
        x: Int,
        y: Int,
        surfaceWidth: Int,
        surfaceHeight: Int,
    )

    /** One key transition. vk: Windows VK (0 = dropped by Rust). mods: VK modifier mask (0 for now). */
    external fun nativeSendKey(handle: Long, vk: Int, down: Boolean, mods: Int)

    /**
     * Whether the host advertised full-fidelity stylus injection (`HOST_CAP_PEN`) — the gate
     * for splitting stylus pointers out of the touch path onto the pen plane. False on `0`.
     */
    external fun nativeHostSupportsPen(handle: Long): Boolean

    /**
     * One stylus batch of STATE-FULL samples (the pen plane; design/pen-tablet-input.md §7):
     * [count] × 10 floats, oldest first — `[state, tool, x, y, pressure, distance, tilt_deg,
     * azimuth_deg, roll_deg, dt_us]`. `state` = the wire in-range/touching/barrel bits; `tool`
     * 0=pen 1=eraser; x/y/pressure/distance normalized 0..1; distance/tilt/azimuth/roll < 0 =
     * unknown. Send only when [nativeHostSupportsPen]; repeat the last sample ≤100 ms while the
     * pen is in range (the host force-releases a silent stroke after 200 ms).
     */
    external fun nativeSendPen(handle: Long, samples: FloatArray, count: Int)

    /**
     * Whether the host advertised committed-text injection (`HOST_CAP_TEXT_INPUT`) — its inject
     * backend can type Unicode text directly. Picks the real IME `InputConnection` (autocorrect,
     * gesture typing, non-Latin scripts) over the TYPE_NULL raw-key fallback. False on `0`.
     */
    external fun nativeTextInputSupported(handle: Long): Boolean

    /**
     * Committed IME text → one `TextInput` wire event per Unicode scalar, in order. Control
     * characters are skipped natively (Enter/Backspace ride [nativeSendKey]). Only meaningful
     * when [nativeTextInputSupported] returned true — older hosts ignore the events.
     */
    external fun nativeSendText(handle: Long, text: String)

    // ---- Shared clipboard (text v1): Kotlin drives ClipboardManager, Rust the protocol ----
    // Opt-in per session (nativeClipControl). Local copies are announced as lazy offers; bytes
    // cross only when the host pastes (a "fetch:" event answered by nativeClipServeText). Host
    // copies arrive as "offer:" events, fetched eagerly into the system clipboard.

    /** Whether the host advertised a working shared-clipboard service (HOST_CAP_CLIPBOARD). */
    external fun nativeClipSupported(handle: Long): Boolean

    /** Session-level clipboard opt-in/out; nothing happens until enabled=true crosses. */
    external fun nativeClipControl(handle: Long, enabled: Boolean)

    /** Announce "this device's clipboard now holds text". [seq]: monotonic, newest wins. */
    external fun nativeClipOfferText(handle: Long, seq: Int)

    /** Pull the text of the host's offer [seq] → transfer id echoed on "data:"/"error:", or -1. */
    external fun nativeClipFetchText(handle: Long, seq: Int): Int

    /** Answer a "fetch:" event with the clipboard's current text (the host is pasting). */
    external fun nativeClipServeText(handle: Long, reqId: Int, text: String)

    /** Abort a clipboard transfer by id (either direction). */
    external fun nativeClipCancel(handle: Long, id: Int)

    /**
     * Block ≤250 ms for the next clipboard event, as a compact string: `state:<0|1>` ·
     * `offer:<seq>:<hasText>` · `fetch:<reqId>` · `data:<xferId>:<text>` · `cancel:<id>` ·
     * `error:<id>:<code>` · `closed` (session gone) — null on timeout. Dedicated poll thread.
     */
    external fun nativeNextClip(handle: Long): String?

    // ---- Gamepad: each controller forwarded on its own wire pad index (0..15, low byte of flags) ----
    // The pad index is assigned per Android device by GamepadRouter; a single controller lands on 0,
    // so its wire is byte-identical to the old single-pad path. The core folds the per-transition
    // events into seq'd GamepadState snapshots keyed on this index and owns the per-pad seq.

    /** One gamepad button transition on wire pad [pad] (0..15). bit: a [Gamepad].BTN_* bit. down: press/release. */
    external fun nativeSendGamepadButton(handle: Long, bit: Int, down: Boolean, pad: Int)

    /** One gamepad axis update on wire pad [pad] (0..15). axisId: [Gamepad].AXIS_* (0..5). value: stick i16 (+y=up) / trigger 0..255. */
    external fun nativeSendGamepadAxis(handle: Long, axisId: Int, value: Int, pad: Int)

    /**
     * Declare the controller KIND presented on wire pad [pad] (0..15) so the host builds a matching
     * virtual device (mixed types across pads). pref: a [Gamepad].PREF_* wire byte. Send ONCE when a
     * pad opens, BEFORE any of its input; an older host ignores it (that pad then uses the handshake's
     * session-default kind — the pre-existing single-pad behaviour on pad 0).
     */
    external fun nativeSendGamepadArrival(handle: Long, pref: Int, pad: Int)

    /** Signal wire pad [pad] (0..15) was unplugged so the host tears its virtual device down. The core stamps the seq + re-sends. */
    external fun nativeSendGamepadRemove(handle: Long, pad: Int)

    /**
     * One raw HID input report from a client-captured controller (the as-is Steam Controller 2
     * passthrough), forwarded verbatim on the rich-input plane. [buf] is a DIRECT ByteBuffer whose
     * first [len] bytes are the report, id byte first (0x42/0x45/0x47 state, 0x43 battery, …);
     * len is clamped to 64. Called from the capture thread at the controller's own report rate.
     */
    external fun nativeSendPadHidReport(handle: Long, pad: Int, buf: java.nio.ByteBuffer, len: Int)

    // ---- Host→client gamepad feedback: Rust pulls block ~100ms, Kotlin renders (see GamepadFeedback) ----

    /**
     * Block up to ~100 ms for the next rumble update. Returns a packed positive long: bits 49..52 =
     * wire pad index (0..15), bit 48 = has a v2 lease, bits 32..47 = ttl_ms, bits 16..31 = low, bits
     * 0..15 = high (each amplitude 0..0xFFFF; 0/0 = stop), or -1 on timeout / session closed. Kotlin
     * routes the update to the controller holding that pad index. Call from a dedicated poll thread.
     */
    external fun nativeNextRumble(handle: Long): Long

    /**
     * Block up to ~100 ms for the next HID-output event, written into [buf] (a direct ByteBuffer,
     * capacity >= 128) as `[pad][kind][fields…]` (leading pad = the wire pad index to route to):
     * Led=pad 01 r g b, PlayerLeds=pad 02 bits, Trigger=pad 03 which effect…, raw as-is
     * passthrough report=pad 05 kind report-bytes (kind 0 = output report, 1 = feature report).
     * Returns the byte count, or -1 on timeout / session closed.
     */
    external fun nativeNextHidout(handle: Long, buf: java.nio.ByteBuffer): Int
}
