package com.dtourolle.jellytau

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest

/**
 * Reports the *kind* of network the device is on, so downloads can be gated on
 * "unmetered only" (the WiFi-only setting).
 *
 * This is deliberately separate from the Rust-side ConnectivityMonitor, which
 * answers a different question: whether the Jellyfin *server* is reachable,
 * derived from real request outcomes. Reachability and transport type are
 * orthogonal — you can be on WiFi with a dead server, or on cellular with a
 * perfectly reachable one.
 *
 * Requires ACCESS_NETWORK_STATE; without it getNetworkCapabilities returns null
 * and we report UNKNOWN (which the gate treats as "not acceptable" when
 * wifi-only is on, failing closed rather than burning mobile data).
 *
 * TRACES: UR-053 | DR-074
 */
object NetworkTypeMonitor {
  private const val TAG = "NetworkTypeMonitor"

  /** Transport classification, mirrored by the Rust `NetworkType` enum. */
  const val TYPE_NONE = "none"
  const val TYPE_WIFI = "wifi"
  const val TYPE_ETHERNET = "ethernet"
  const val TYPE_CELLULAR = "cellular"
  const val TYPE_OTHER = "other"
  const val TYPE_UNKNOWN = "unknown"

  private var callback: ConnectivityManager.NetworkCallback? = null

  /** Invoked on any network change; set by [startWatching]. */
  @Volatile
  private var onChange: (() -> Unit)? = null

  private fun connectivityManager(context: Context): ConnectivityManager? =
    context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

  /**
   * Current transport type of the active network.
   *
   * Returns UNKNOWN (not NONE) when capabilities can't be read, so callers can
   * distinguish "definitely offline" from "couldn't tell".
   */
  fun currentType(context: Context): String {
    val cm = connectivityManager(context) ?: return TYPE_UNKNOWN
    val network = cm.activeNetwork ?: return TYPE_NONE
    val caps = cm.getNetworkCapabilities(network) ?: return TYPE_UNKNOWN

    return when {
      caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> TYPE_WIFI
      caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> TYPE_ETHERNET
      caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> TYPE_CELLULAR
      else -> TYPE_OTHER
    }
  }

  /**
   * Whether the active network is unmetered.
   *
   * This is the bit that actually matters for the WiFi-only gate: a phone
   * hotspot reports TRANSPORT_WIFI but is metered, and is backed by exactly the
   * cellular data the setting exists to protect. Checking NOT_METERED rather
   * than the transport alone means tethering doesn't quietly burn a data plan.
   */
  fun isUnmetered(context: Context): Boolean {
    val cm = connectivityManager(context) ?: return false
    val network = cm.activeNetwork ?: return false
    val caps = cm.getNetworkCapabilities(network) ?: return false
    return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
  }

  /**
   * Whether downloads may run right now given the wifi-only preference.
   *
   * Ethernet counts as acceptable (it is unmetered in practice and is what
   * Android TV devices use). Cellular never does. When wifi-only is off this is
   * always true — the gate simply isn't engaged.
   */
  fun isAcceptable(context: Context, wifiOnly: Boolean): Boolean {
    if (!wifiOnly) return true
    val type = currentType(context)
    if (type == TYPE_CELLULAR || type == TYPE_NONE || type == TYPE_UNKNOWN) return false
    // WiFi/Ethernet/other: require unmetered so metered hotspots are excluded.
    return isUnmetered(context)
  }

  /**
   * Register a callback that fires whenever the network changes, so a blocked
   * download queue can be re-pumped the moment an acceptable network appears.
   * Without this the queue would stall until some unrelated event pumped it.
   *
   * Idempotent: a second call replaces the previous callback.
   */
  fun startWatching(context: Context, onNetworkChanged: () -> Unit) {
    val cm = connectivityManager(context) ?: run {
      android.util.Log.w(TAG, "No ConnectivityManager; network changes won't be observed")
      return
    }

    stopWatching(context)
    onChange = onNetworkChanged

    val request = NetworkRequest.Builder()
      .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
      .build()

    val cb = object : ConnectivityManager.NetworkCallback() {
      override fun onAvailable(network: Network) {
        android.util.Log.d(TAG, "Network available")
        onChange?.invoke()
      }

      override fun onLost(network: Network) {
        android.util.Log.d(TAG, "Network lost")
        onChange?.invoke()
      }

      override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
        // Fires when e.g. metered-ness flips without the network itself changing.
        onChange?.invoke()
      }
    }

    try {
      cm.registerNetworkCallback(request, cb)
      callback = cb
      android.util.Log.d(TAG, "Network callback registered")
    } catch (e: Exception) {
      android.util.Log.e(TAG, "Failed to register network callback", e)
    }
  }

  /** Unregister the network callback, if one is active. */
  fun stopWatching(context: Context) {
    val cb = callback ?: return
    val cm = connectivityManager(context)
    try {
      cm?.unregisterNetworkCallback(cb)
    } catch (e: Exception) {
      android.util.Log.w(TAG, "Failed to unregister network callback", e)
    }
    callback = null
    onChange = null
  }
}
