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

  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)

    // 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)
    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 {
      val webView = 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

      // Register the @JavascriptInterface bridges EXACTLY ONCE per WebView.
      //
      // configureWebViewForMedia() runs from onCreate's delayed post AND from
      // every onResume (plus each WebView re-find), so this used to re-inject
      // all four bridges 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.
      //
      // The settings/WebChromeClient work below is idempotent and must keep
      // running on resume; only the bridge injection is one-shot.

      // 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)

      if (webView === bridgesInstalledOn) {
        android.util.Log.d("MainActivity", "JS bridges already installed on this WebView - skipping re-injection")
        configureWebViewSettings(webView)
        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
        }
      }, "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 {
            val color = if (transparent) {
              android.graphics.Color.TRANSPARENT
            } else {
              android.graphics.Color.BLACK
            }
            mediaWebView?.setBackgroundColor(color)
            // The WebView's window/surface must also stop painting opaque, or a
            // hardware-accelerated WebView still composites its own background.
            window.setBackgroundDrawable(
              android.graphics.drawable.ColorDrawable(color)
            )
            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")

      // 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")
      }

      configureWebViewSettings(webView)

    } catch (e: Exception) {
      android.util.Log.e("MainActivity", "Failed to configure WebView for media", 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
        allowFileAccess = true
        allowContentAccess = true
        mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW

        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
  }

}
