package com.dtourolle.jellytau

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.view.View
import androidx.activity.enableEdgeToEdge


class MainActivity : TauriActivity() {
  private val handler = Handler(Looper.getMainLooper())
  private var configAttempts = 0
  private val maxConfigAttempts = 10

  /**
   * Coarse override for whether backgrounding the app should auto-enter PiP.
   *
   * This is NOT what excludes audio/browsing/cast from PiP — that is the
   * PictureInPictureManager.canEnterPip guard, which requires a local video
   * surface to be actively rendering and is re-checked in onUserLeaveHint. This
   * flag is only toggled by the background-audio feature (via
   * AndroidPictureInPicture.setAutoEnterEnabled) so background-audio mode and
   * auto-PiP stay mutually exclusive.
   */
  @Volatile
  private var autoEnterPipEnabled = true

  /**
   * Whether the user armed background-audio mode on the current video (UR-040).
   * When true, leaving the app hands audio off to the native ExoPlayer audio
   * service (frontend-driven) instead of entering PiP, and video decode stops.
   * The frontend sets this via AndroidBackgroundAudio.setEnabled.
   */
  @Volatile
  private var backgroundAudioEnabled = false

  /**
   * The WebView carrying the Svelte UI, cached once found so lifecycle overrides
   * can dispatch DOM events into it (native → frontend signalling).
   */
  private var mediaWebView: WebView? = null

  /**
   * The WebView the @JavascriptInterface bridges have been injected into.
   *
   * addJavascriptInterface must run once per WebView instance: re-injecting
   * over an already-loaded page hands JS a stale proxy whose methods are gone.
   * Compared by identity so a genuinely new WebView still gets its bridges.
   */
  private var bridgesInstalledOn: WebView? = null

  /**
   * wry hands us the WebView here, and this is the only point at which the
   * bridges can be installed *deterministically*.
   *
   * WebView binds an injected object into JS at **page-load time**: an
   * addJavascriptInterface call that lands after the page has loaded does not
   * appear to that page at all. The bridges used to be installed from
   * [configureWebViewForMedia], which finds the WebView by walking the view
   * tree 500 ms after onCreate — a race against Tauri's own page load, and one
   * that is *permanent* when lost, because the identity guard then declines to
   * re-inject on the resume passes. The whole set (`AndroidVideoSurface`,
   * `AndroidPictureInPicture`, `AndroidBackgroundAudio`, `AndroidNetworkType`,
   * `AndroidImmersive`, `AndroidInsets`) simply would not exist in `window`,
   * silently: every one of them is called through an optional chain, so a
   * missing bridge is a no-op rather than an error. That is a candidate
   * explanation for DR-172's central piece of evidence — native video shipped
   * with `WebView transparent = false` logged and `= true` never appearing,
   * i.e. the enable call never reaching Kotlin.
   *
   * `WryActivity.setWebView()` calls this immediately before wry issues the
   * first `loadUrl`, so a bridge installed here is bound by the time any page
   * runs. Note this can fire during `super.onCreate()`, i.e. *before* the rest
   * of our own onCreate — so only work that needs nothing but the WebView
   * belongs here. Insets are deliberately left to
   * [configureWebViewForMedia], which runs later and on every resume.
   *
   * TRACES: UR-003, UR-004 | DR-183
   */
  override fun onWebViewCreate(webView: WebView) {
    super.onWebViewCreate(webView)
    android.util.Log.d("MainActivity", "onWebViewCreate - installing bridges before first page load")
    mediaWebView = webView
    // A new WebView means a new page, which reports no video yet. Anything the
    // previous one left held would otherwise pin the screen on for the life of
    // the process, since a page that goes away never sends its final
    // setHtml5VideoState(false, …). (DR-202)
    ScreenWakeManager.releaseAll()
    installJavascriptBridges(webView)
    configureWebViewSettings(webView)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    enableEdgeToEdge()
    super.onCreate(savedInstanceState)

    // enableEdgeToEdge() puts the WebView under the status bar, the navigation/
    // gesture bar and the display cutout — and targeting SDK 36 makes that
    // non-optional anyway. Android WebView never surfaces the *system bar*
    // insets to CSS (only the display cutout), so the web layer has to be told.
    // Without this the bottom nav renders underneath the navigation bar, badly
    // so on devices with a tall opaque 3-button bar. (UR-066)
    WindowInsetsBridge.install(this)

    // Hand the player an Activity reference so it can attach its video
    // SurfaceView to the content view behind the WebView.
    //
    // Without this, JellyTauPlayer.currentActivity stays null forever and
    // autoAttachSurface() logs "Cannot attach surface - no Activity reference"
    // and returns — so the SurfaceView is created, wired to ExoPlayer, and then
    // never added to the view hierarchy. Native video decoded to a surface that
    // was never on screen. setActivity() stores into a companion-object
    // WeakReference, so calling it here (before Rust initializes the player over
    // JNI) is safe and is the case it was written for.
    //
    // TRACES: UR-003, UR-041 | DR-151
    com.dtourolle.jellytau.player.JellyTauPlayer.setActivity(this)

    // The window whose FLAG_KEEP_SCREEN_ON is toggled while video plays. Set on
    // every onCreate so a recreated Activity (rotation) re-applies the current
    // hold to its new window. (UR-003, DR-202)
    ScreenWakeManager.setActivity(this)

    // Configure WebView for media playback after Tauri initialization
    handler.postDelayed({
      configureWebViewForMedia()
    }, 500)
  }

  override fun onResume() {
    super.onResume()
    configureWebViewForMedia()
  }

  /**
   * Called when the user leaves the app via Home or the gesture equivalent
   * (but NOT via Back). This is the standard hook for auto-entering PiP so
   * video keeps playing in a floating window instead of being backgrounded.
   *
   * TRACES: UR-041 | IR-026 | DR-053
   */
  override fun onUserLeaveHint() {
    super.onUserLeaveHint()
    // Never enter PiP while background-audio mode is armed — the two are mutually
    // exclusive (the handoff runs from onStop instead).
    if (autoEnterPipEnabled && !backgroundAudioEnabled &&
        PictureInPictureManager.canEnterPip(this)) {
      android.util.Log.d("MainActivity", "User leaving with video active - entering PiP")
      PictureInPictureManager.enterPip(this)
    }
  }

  /**
   * The app is no longer visible (Home, app switch, or screen lock). When
   * background-audio mode is armed, tell the frontend to hand video playback off
   * to the native audio service. onStop (rather than onUserLeaveHint) is used
   * because it fires on screen-lock too, which is the primary use case (UR-040).
   *
   * TRACES: UR-040 | IR-025
   */
  override fun onStop() {
    super.onStop()
    if (backgroundAudioEnabled) {
      dispatchWebEvent("jellytau-background")
    }
  }

  /** The app is visible again — tell the frontend to resume WebView video. */
  override fun onStart() {
    super.onStart()
    if (backgroundAudioEnabled) {
      dispatchWebEvent("jellytau-foreground")
    }
  }

  /**
   * Dispatch a DOM CustomEvent into the WebView (native → frontend). Mirrors the
   * evaluateJavascript pattern already used to unmute video elements. Posted to
   * the WebView thread; safe no-op if the WebView isn't found yet.
   */
  private fun dispatchWebEvent(name: String) {
    val webView = mediaWebView ?: run {
      android.util.Log.w("MainActivity", "dispatchWebEvent('$name'): no WebView")
      return
    }
    webView.post {
      webView.evaluateJavascript(
        "window.dispatchEvent(new CustomEvent('$name'));",
        null
      )
      android.util.Log.d("MainActivity", "Dispatched web event: $name")
    }
  }

  override fun onDestroy() {
    NetworkTypeMonitor.stopWatching(this)
    ScreenWakeManager.clearActivity(this)
    super.onDestroy()
  }

  override fun onPictureInPictureModeChanged(
    isInPictureInPictureMode: Boolean,
    newConfig: android.content.res.Configuration
  ) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    android.util.Log.d("MainActivity", "PiP mode changed: $isInPictureInPictureMode")
    PictureInPictureManager.onPipModeChanged(this, isInPictureInPictureMode)
  }

  private fun configureWebViewForMedia() {
    try {
      // onWebViewCreate normally got here first; the tree walk is the fallback
      // for a WebView we were never handed.
      val webView = mediaWebView ?: findWebView(window.decorView)

      if (webView == null) {
        android.util.Log.w("MainActivity", "WebView not found (attempt ${configAttempts + 1}/$maxConfigAttempts)")
        if (configAttempts < maxConfigAttempts) {
          configAttempts++
          handler.postDelayed({
            configureWebViewForMedia()
          }, 200)
        } else {
          android.util.Log.e("MainActivity", "Failed to find WebView after $maxConfigAttempts attempts")
        }
        return
      }

      android.util.Log.d("MainActivity", "WebView found! Configuring settings...")
      mediaWebView = webView

      // Re-push the safe-area insets. Unlike addJavascriptInterface this is
      // idempotent and MUST re-run: a page load discards the inline style the
      // last push set, so the WebView would otherwise be left with no insets.
      WindowInsetsBridge.attachWebView(webView)

      // Normally already done by onWebViewCreate; this is the fallback path.
      installJavascriptBridges(webView)
      configureWebViewSettings(webView)

    } catch (e: Exception) {
      android.util.Log.e("MainActivity", "Failed to configure WebView for media", e)
    }
  }

  /**
   * Register the @JavascriptInterface bridges EXACTLY ONCE per WebView.
   *
   * This runs from [onWebViewCreate] — the only point early enough to be bound
   * before the first page load — and from [configureWebViewForMedia] as a
   * fallback. The latter runs from onCreate's delayed post AND from every
   * onResume (plus each WebView re-find), so without the identity guard this
   * re-injected every bridge repeatedly — 5 times in a 45s session. WebView
   * binds injected objects at page-load time; re-injecting over a live page
   * leaves JS holding a stale proxy. The object stays truthy while its methods
   * vanish, which surfaced as a flood of "WebView: Unknown object" chromium
   * errors and, in JS, "TypeError: setEnabled is not a function".
   *
   * The visible bug: the background-audio toggle turned blue but never reached
   * native, so backgroundAudioEnabled stayed false, onStop never dispatched
   * 'jellytau-background', and a locked screen killed audio instantly (UR-040).
   * Audio focus and PiP broke the same way.
   *
   * Settings/WebChromeClient work is idempotent and must keep running on
   * resume, so it lives in [configureWebViewSettings], not here.
   *
   * TRACES: UR-003, UR-004, UR-040, UR-041 | DR-183
   */
  private fun installJavascriptBridges(webView: WebView) {
    try {
      if (webView === bridgesInstalledOn) {
        android.util.Log.d("MainActivity", "JS bridges already installed on this WebView - skipping re-injection")
        return
      }
      bridgesInstalledOn = webView

      // NOTE: there is deliberately no "AndroidAudioFocus" bridge. Manual focus
      // requests from the WebView competed with Chromium's own
      // AudioFocusDelegate and with ExoPlayer, and the resulting
      // AUDIOFOCUS_LOSS paused playback. See the comment on the video listeners
      // in configureWebViewSettings().

      // Add JavaScript interface for picture-in-picture control.
      // enterPip/canEnterPip must run on the main thread; @JavascriptInterface
      // methods are invoked on a WebView binder thread.
      webView.addJavascriptInterface(object : Any() {
        @JavascriptInterface
        fun enterPip() {
          handler.post { PictureInPictureManager.enterPip(this@MainActivity) }
        }

        /** Whether the PiP button should be offered in the player UI at all. */
        @JavascriptInterface
        fun isSupported(): Boolean {
          return PictureInPictureManager.isPipSupported(this@MainActivity)
        }

        /** Whether entering PiP would work right now (local video playing). */
        @JavascriptInterface
        fun canEnterPip(): Boolean {
          return PictureInPictureManager.canEnterPip(this@MainActivity)
        }

        /** Let the frontend opt out of auto-PiP (e.g. while casting). */
        @JavascriptInterface
        fun setAutoEnterEnabled(enabled: Boolean) {
          autoEnterPipEnabled = enabled
        }

        /**
         * Report the WebView `<video>` state.
         *
         * Without this PiP only ever knew about the native ExoPlayer surface,
         * which is behind an experimental flag that defaults to off — so in the
         * shipping configuration nothing ever satisfied canEnterPip and the
         * button did nothing. (DR-160)
         */
        @JavascriptInterface
        fun setHtml5VideoState(active: Boolean, width: Int, height: Int, playing: Boolean) {
          PictureInPictureManager.setHtml5VideoState(active, width, height, playing)
          // The same report is what keeps the display awake on the webview
          // rendering path — the WebView takes no display wake lock of its own
          // for `<video>`. (DR-202)
          ScreenWakeManager.onHtml5VideoState(active, playing)
        }
      }, "AndroidPictureInPicture")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidPictureInPicture' added")

      // Add JavaScript interface for background-audio mode (UR-040). The frontend
      // arms/disarms it via the player toggle; the Activity uses the flag in its
      // lifecycle overrides to decide between the audio handoff and PiP.
      webView.addJavascriptInterface(object : Any() {
        /** Frontend arms/disarms background-audio mode for the current video. */
        @JavascriptInterface
        fun setEnabled(enabled: Boolean) {
          backgroundAudioEnabled = enabled
          android.util.Log.d("MainActivity", "backgroundAudioEnabled = $enabled")
        }
      }, "AndroidBackgroundAudio")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidBackgroundAudio' added")

      // Network transport reporting for the WiFi-only download gate (UR-053).
      // The frontend polls these on demand and re-pumps the download queue when
      // the 'jellytau-network-changed' event fires.
      webView.addJavascriptInterface(object : Any() {
        /** Active transport: wifi | ethernet | cellular | other | none | unknown. */
        @JavascriptInterface
        fun currentType(): String = NetworkTypeMonitor.currentType(this@MainActivity)

        /** Whether the active network is unmetered. */
        @JavascriptInterface
        fun isUnmetered(): Boolean = NetworkTypeMonitor.isUnmetered(this@MainActivity)

        /** Whether downloads may run given the wifi-only preference. */
        @JavascriptInterface
        fun isAcceptable(wifiOnly: Boolean): Boolean =
          NetworkTypeMonitor.isAcceptable(this@MainActivity, wifiOnly)

        /** Whether native network detection is available at all (false on non-Android). */
        @JavascriptInterface
        fun isSupported(): Boolean = true
      }, "AndroidNetworkType")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidNetworkType' added")

      // Native video compositing: let the frontend make the WebView transparent
      // so the ExoPlayer SurfaceView behind it is visible (UR-003, UR-004).
      //
      // Toggled rather than set once because a transparent WebView is only
      // correct while a native video is on screen — every other screen needs its
      // opaque background, and leaving the window transparent shows the
      // launcher/wallpaper through the app.
      //
      // The CSS in app.css clears the *web* layer's backgrounds; this clears the
      // WebView widget's own background, which CSS cannot reach. Both are
      // required — an opaque WebView hides the surface no matter what the page
      // paints.
      //
      // TRACES: UR-003, UR-004 | DR-150
      webView.addJavascriptInterface(object : Any() {
        /** Make the WebView background transparent (true) or opaque (false). */
        @JavascriptInterface
        fun setTransparent(transparent: Boolean) {
          handler.post {
            mediaWebView?.setBackgroundColor(
              if (transparent) {
                android.graphics.Color.TRANSPARENT
              } else {
                android.graphics.Color.BLACK
              }
            )
            // The WINDOW background stays OPAQUE — including while compositing.
            // It is the only thing that paints the pixels the video does not
            // cover, and clearing it was the whole defect.
            //
            // This window's surface is opaque: the theme is not translucent and
            // `dumpsys window` shows no translucency flag on it. For an opaque
            // surface HWUI deliberately does NOT clear the damaged region before
            // replaying a frame — it assumes the view hierarchy paints every
            // pixel it owns. That hierarchy is: window background, then the video
            // TextureView, then this transparent WebView. `fitSurfaceToScreen`
            // sizes the TextureView to the *letterboxed* video rect, so the bars
            // around the video are painted by the window background and nothing
            // else.
            //
            // Setting that background TRANSPARENT therefore left the bars painted
            // by nobody, and stale framebuffer content simply survived in them:
            // a whole ghost copy of the control bar stranded in the top bar, and
            // each new clock digit composited over the one before it ("35:42"
            // with the 1 still showing through the 2). The rotation flash is the
            // same bug at full-screen scale — the pre-rotation image persisting
            // in what became the new bars — which is why neither
            // ROTATION_ANIMATION_JUMPCUT nor revealing on frame arrival ever
            // touched it. Both were aimed at the window animation; the pixels
            // were never the animation's.
            //
            // The WebView's own background, set above, is what lets the video
            // through. An opaque window background cannot hide it: the
            // TextureView is drawn on top of it, not under it.
            //
            // TRACES: UR-003, UR-066 | DR-194
            window.setBackgroundDrawable(
              android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK)
            )
            android.util.Log.d("MainActivity", "WebView transparent = $transparent")
          }
        }

        /** Whether native-video compositing is available on this platform. */
        @JavascriptInterface
        fun isSupported(): Boolean = true
      }, "AndroidVideoSurface")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidVideoSurface' added")

      // Full-screen video: hide the system bars (UR-066). requestFullscreen()
      // inside a WebView cannot touch the Activity window, so without this the
      // status and navigation bars stayed painted over full-screen video.
      webView.addJavascriptInterface(object : Any() {
        /** Hide the system bars for full-screen playback. */
        @JavascriptInterface
        fun enter() {
          handler.post { ImmersiveModeBridge.enter(this@MainActivity) }
        }

        /** Restore the system bars on leaving fullscreen or the player. */
        @JavascriptInterface
        fun exit() {
          handler.post { ImmersiveModeBridge.exit(this@MainActivity) }
        }

        /** Whether native immersive mode exists (false on non-Android). */
        @JavascriptInterface
        fun isSupported(): Boolean = true
      }, "AndroidImmersive")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidImmersive' added")

      // Window insets (safe areas). The push path above races the page load, so
      // the frontend pulls the current values on mount through this bridge.
      webView.addJavascriptInterface(WindowInsetsBridge.jsInterface(), "AndroidInsets")
      android.util.Log.d("MainActivity", "JavaScript interface 'AndroidInsets' added")

      // Push network changes into the WebView so a queue blocked on "waiting for
      // WiFi" resumes the moment an acceptable network appears.
      NetworkTypeMonitor.startWatching(this) {
        dispatchWebEvent("jellytau-network-changed")
      }

    } catch (e: Exception) {
      android.util.Log.e("MainActivity", "Failed to install JavaScript bridges", e)
    }
  }

  /**
   * WebView settings, chrome client and the video-unmute script.
   *
   * Split out from the bridge injection because this half is idempotent and
   * must re-run on every resume, whereas addJavascriptInterface must not.
   */
  private fun configureWebViewSettings(webView: WebView) {
    try {
      // Set WebChromeClient to handle video playback and audio focus
      webView.webChromeClient = object : WebChromeClient() {
        override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
          super.onShowCustomView(view, callback)
          android.util.Log.d("MainActivity", "Video entered fullscreen")
        }

        override fun onHideCustomView() {
          super.onHideCustomView()
          android.util.Log.d("MainActivity", "Video exited fullscreen")
        }

        /**
         * Forward WebView console output to logcat under the "JellyTauWeb" tag.
         *
         * Without this the frontend is invisible to `adb logcat`, which makes
         * diagnosing anything that spans the JS/native boundary (the
         * background-audio handoff in particular) guesswork.
         */
        override fun onConsoleMessage(msg: android.webkit.ConsoleMessage): Boolean {
          android.util.Log.d(
            "JellyTauWeb",
            "${msg.message()} (${msg.sourceId()}:${msg.lineNumber()})"
          )
          return true
        }
      }
      android.util.Log.d("MainActivity", "WebChromeClient configured")

      webView.settings.apply {
        // CRITICAL: Enable media playback without user gesture requirement
        mediaPlaybackRequiresUserGesture = false
        android.util.Log.d("MainActivity", "Set mediaPlaybackRequiresUserGesture = false")

        javaScriptEnabled = true
        domStorageEnabled = true

        // The three settings below used to read
        //   allowFileAccess = true
        //   allowContentAccess = true
        //   mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW
        // which handed the webview a blanket cleartext opt-in and undid
        // res/xml/network_security_config.xml, whose whole point is that only
        // 127.0.0.1 is exempt from the cleartext ban and that this "must not
        // become a blanket cleartext opt-in" (DR-138). Nothing needed any of it:
        //
        // - `file://` is never loaded. Cached thumbnails go through
        //   `convertFileSrc` (imageCache.ts), which on Android resolves to
        //   `http://asset.localhost/...` — a Tauri custom protocol answered by
        //   wry's request interceptor, not the filesystem. Downloaded media goes
        //   through `media_local_url` → the loopback HTTP server on 127.0.0.1
        //   (media_server.rs, DR-137), which exists precisely *because* the
        //   asset/file route cannot stream a large file.
        // - `content://` is never loaded either. The manifest's FileProvider is
        //   for outbound share intents, not for webview navigation.
        // - Mixed content never arises. Tauri serves the UI from
        //   `http://tauri.localhost` (`use_https_scheme` is false by default and
        //   is not set in tauri.conf.json), and both the loopback media server
        //   and `asset.localhost` are loopback/`.localhost` origins, which
        //   Chromium treats as potentially trustworthy — so they are not mixed
        //   content in the first place. A plain-HTTP *remote* Jellyfin server
        //   would be, but the network security config already rejects it before
        //   the mixed-content check is ever reached, so ALWAYS_ALLOW bought
        //   nothing and only widened the hole.
        //
        // COMPATIBILITY_MODE rather than NEVER_ALLOW is a deliberate hedge, not
        // the default: the platform default at targetSdk 21+ is NEVER_ALLOW, so
        // this is still one step looser than "stop overriding". It keeps passive
        // content (images) working if some path the analysis above missed turns
        // out to need it, which matters because this change cannot be verified
        // anywhere but a device. Tighten to NEVER_ALLOW once offline video and
        // cached artwork are confirmed on real hardware.
        //
        // `allowFileAccess = false` is the targetSdk-30+ platform default being
        // restored; `allowContentAccess = false` is a genuine tightening (its
        // default is true) and is the one to look at first if anything that used
        // to render stops.
        //
        // TRACES: UR-071 | DR-199
        allowFileAccess = false
        allowContentAccess = false
        mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE

        android.util.Log.d("MainActivity", "WebView fully configured for media playback")
      }

      // Execute JavaScript to ensure any video elements are unmuted and request audio focus
      webView.post {
        webView.evaluateJavascript("""
          (function() {
            console.log('[Android] Ensuring video elements are unmuted');
            const videos = document.getElementsByTagName('video');
            for (let video of videos) {
              video.muted = false;
              video.volume = 1.0;
              console.log('[Android] Video unmuted, volume:', video.volume, 'muted:', video.muted);

              // NOTE: deliberately no audio-focus calls here.
              //
              // WebView already manages audio focus for <video> through
              // Chromium's own AudioFocusDelegate. Requesting AUDIOFOCUS_GAIN
              // again from MainActivity made two requesters compete inside one
              // uid: the grant was immediately followed by AUDIOFOCUS_LOSS
              // (~45ms), whose handler paused playback - so arming background
              // audio, or simply pressing play, paused the video in a loop.
              //
              // ExoPlayer is the third potential owner and stays authoritative
              // for native playback (JellyTauPlayer manages its own focus).
              // Leave focus to whichever engine is actually rendering.
              video.addEventListener('volumechange', function() {
                console.log('[Android] Video volume changed - volume:', this.volume, 'muted:', this.muted);
              });
            }

            // Monitor for new video elements
            const observer = new MutationObserver(() => {
              const videos = document.getElementsByTagName('video');
              for (let video of videos) {
                if (video.muted) {
                  video.muted = false;
                  video.volume = 1.0;
                  console.log('[Android] New video found and unmuted');
                }
              }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            console.log('[Android] Video unmute observer installed');
          })();
        """.trimIndent(), null)
      }

    } catch (e: Exception) {
      android.util.Log.e("MainActivity", "Failed to configure WebView for media", e)
    }
  }

  private fun findWebView(view: android.view.View): WebView? {
    if (view is WebView) {
      android.util.Log.d("MainActivity", "Found WebView!")
      return view
    }

    if (view is android.view.ViewGroup) {
      for (i in 0 until view.childCount) {
        val child = view.getChildAt(i)
        val webView = findWebView(child)
        if (webView != null) {
          return webView
        }
      }
    }

    return null
  }

}
