package chat.vojo.app;

import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.core.graphics.Insets;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {
    public static volatile boolean isInForeground = false;
    private static volatile boolean launchSplashReady = false;

    // Safety net for setKeepOnScreenCondition: if JS never calls
    // launchSplash.ready() (boot crash, exception during config load before
    // AuthMascot mounts, network hang in useClientConfig, deep-link straight
    // into AuthLayout where the centered AuthMascot variant doesn't render,
    // …) the splash would otherwise hang indefinitely and the user can't
    // interact with anything. 5s covers normal cold boots on mid-range
    // Android (config + bundle parse + first paint typically lands inside
    // 1-2s) with comfortable headroom; past it we drop the splash and let
    // whatever the web side has rendered take over — including blank
    // AuthLayout, which is at least recoverable.
    private static final long SPLASH_SAFETY_TIMEOUT_MS = 5000L;

    // Short debounce on the onPause→renderRegistry edge so an in-flight JS
    // removeIncomingRing bridge call (e.g. user accepted/declined, then
    // immediately pressed Home) has a chance to land before we post native
    // CallStyle for a ring that's about to be removed. 150ms covers the
    // strip-accept chain (Capacitor roundtrip + switchOrStartDmCall
    // resolve + sync-effect → bridge) on mid-range Android; imperceptible
    // as a silent-ring delay.
    private static final long RENDER_DEBOUNCE_MS = 150L;
    // Modest debounce on the onResume→cancelRenderedIncomingRings edge so JS
    // has a moment to hydrate incomingCallsAtom before the native surface
    // goes away. Covers warm resume (hook alive, /sync delivers an ADD
    // within ~100-200ms) cleanly. Cold resume (killed process, Matrix
    // client rehydration takes 1-3s) still has a "no surface" window —
    // acceptable tradeoff since tap-native flows carry call_action and
    // are handled by pendingCallActionConsumer regardless of atom state.
    private static final long CANCEL_DEBOUNCE_MS = 300L;
    private final Handler lifecycleHandler = new Handler(Looper.getMainLooper());
    private final Runnable renderRunnable = () ->
        VojoFirebaseMessagingService.renderRegistry(this);
    private final Runnable cancelRunnable = () ->
        VojoFirebaseMessagingService.cancelRenderedIncomingRings(this);

    public static void releaseLaunchSplash() {
        launchSplashReady = true;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            launchSplashReady = false;
        }

        // Custom plugins must be registered before super.onCreate so BridgeActivity
        // can wire them into the WebView bridge on load. Registering after
        // super.onCreate would make the plugin invisible to JS until the next relaunch.
        registerPlugin(FullScreenIntentPlugin.class);
        registerPlugin(CallForegroundPlugin.class);
        registerPlugin(AudioRoutePlugin.class);
        registerPlugin(LaunchSplashPlugin.class);
        registerPlugin(ShareTargetPlugin.class);
        registerPlugin(PollingPlugin.class);
        registerPlugin(ProxyPlugin.class);

        // AndroidX SplashScreen must be installed before super.onCreate().
        // Keep it until the web splash confirms its first visible frame is
        // ready, OR the safety timeout elapses (see SPLASH_SAFETY_TIMEOUT_MS).
        final long splashStartMs = System.currentTimeMillis();
        SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
        splashScreen.setKeepOnScreenCondition(() -> {
            if (launchSplashReady) return false;
            return System.currentTimeMillis() - splashStartMs < SPLASH_SAFETY_TIMEOUT_MS;
        });

        EdgeToEdge.enable(this);
        super.onCreate(savedInstanceState);
        // Force light icons on both system bars: our CSS is permanently dark
        // (Dawn redesign), but EdgeToEdge.enable auto-detects icon tint from
        // the device uiMode — on a light-mode device that gives dark icons
        // over our dark bars and they vanish.
        WindowInsetsControllerCompat controller =
            WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
        controller.setAppearanceLightStatusBars(false);
        controller.setAppearanceLightNavigationBars(false);

        // 3-button nav clearance. Reads `tappableElement` (= 0 in gesture
        // mode, = nav-bar height in 3-button mode) and applies it as
        // padding on the activity's content root — NOT on the WebView
        // itself. The WebView is a child of the content root, so root
        // padding shrinks the WebView's layout area; in 3-button mode
        // the WebView ends above the nav bar and the activity
        // windowBackground strip behind it paints `splash_bg` (#0d0e11)
        // which matches the dark body bg, so system icons read as
        // continuous with the chat surface. Gesture mode stays
        // edge-to-edge (padding = 0). Left/right are included for
        // landscape 3-button mode where the nav bar rotates to a side.
        //
        // CRITICAL: the listener MUST live on the content root, not on
        // the WebView. Attaching it to the WebView replaces WebView's
        // internal `OnApplyWindowInsetsListener` (the one Chromium uses
        // to feed CSS `env(safe-area-inset-*)`), which silently breaks
        // `env(safe-area-inset-top)` → `--vojo-safe-top` → every top-
        // anchored UI clearance for the status bar. Padding the parent
        // leaves WebView's window-insets pipeline untouched while still
        // shrinking its visual area.
        //
        // Why `tappableElement` and not `systemBars()` / `navigationBars()`:
        // both report ~24-32 dp in gesture mode too (the pill area),
        // which would lift UI in fullscreen — exactly the regression
        // commit 443213b4 had. `tappableElement` is the type defined as
        // «system UI regions where the user can tap», which means 0 in
        // gesture mode and the nav-bar in 3-button mode.
        //
        // Capacitor 8.3's bundled `SystemBars` plugin attaches its own
        // inset listener on `webView.getParent()` (CoordinatorLayout one
        // level deeper). Since `index.html` declares `viewport-fit=cover`,
        // Capacitor takes the passthrough branch and doesn't pad — no
        // compound-padding with our listener. If `viewport-fit=cover` is
        // ever removed, set `plugins.SystemBars.insetsHandling = "disable"`
        // in `capacitor.config.ts` to avoid double-lift.
        //
        // Fallback for API < 29 (Android 7-9): `tappableElement` did
        // not exist before Q. AndroidX backports the call to API 24+ as
        // `getSystemWindowInsets()` (≈ `navigationBars()`); the explicit
        // gate makes the intent visible at the call site.
        final View contentRoot = findViewById(android.R.id.content);
        ViewCompat.setOnApplyWindowInsetsListener(contentRoot, (v, windowInsets) -> {
            final int typeMask = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                ? WindowInsetsCompat.Type.tappableElement()
                : WindowInsetsCompat.Type.navigationBars();
            Insets ins = windowInsets.getInsets(typeMask);
            v.setPadding(ins.left, 0, ins.right, ins.bottom);
            // Do NOT consume — propagate to WebView (CSS env() pipeline)
            // and Capacitor's SystemBars listener so they still see
            // unmodified insets.
            return windowInsets;
        });
        ViewCompat.requestApplyInsets(contentRoot);
    }

    @Override
    public void onResume() {
        super.onResume();
        isInForeground = true;
        // Cancel any pending render: user came back before the debounce fired,
        // JS strip will own UX, no need to surface native.
        lifecycleHandler.removeCallbacks(renderRunnable);
        // Defer the native cancel so JS strip has a moment to hydrate from
        // incomingCallsAtom. Registry entries persist — they still represent
        // live rings, just the native surfaces go.
        lifecycleHandler.removeCallbacks(cancelRunnable);
        lifecycleHandler.postDelayed(cancelRunnable, CANCEL_DEBOUNCE_MS);
    }

    @Override
    public void onPause() {
        super.onPause();
        isInForeground = false;
        // Re-backgrounding: don't cancel a native the user will still need
        // visible. The render runnable below will re-render if needed.
        lifecycleHandler.removeCallbacks(cancelRunnable);
        // Schedule render — user is backgrounding, JS audio gate is about to
        // close, native CallStyle must surface or the ring goes silent. Debounce
        // absorbs the bridge-call race: if onResume fires within RENDER_DEBOUNCE_MS
        // (user bounce), the render is cancelled.
        lifecycleHandler.removeCallbacks(renderRunnable);
        lifecycleHandler.postDelayed(renderRunnable, RENDER_DEBOUNCE_MS);
    }

    @Override
    public void onDestroy() {
        // Drop any pending render/cancel so runnables — which capture `this` —
        // can't fire post-destroy and land an nm.notify / nm.cancel against a
        // dead Activity context on config change (rotation) or process teardown.
        lifecycleHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}
