package com.foxx.androidcast;

/*********************************************************************
 * AppPreferences.java
 * Created at: Sun 17 May 2026 18:17:14 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class AppPreferences
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (10 commits, 349 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	86ebfc1c70fdfd77db43accfd8ffd2f77d080c321852157a25b9dcedcf806a8b
**********************************************************************/
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;

import com.foxx.androidcast.display.ExternalDisplayCapturePolicy;
import com.foxx.androidcast.receiver.av.ReceiverAudioPreset;
import com.foxx.androidcast.receiver.av.ReceiverVideoPreset;
import com.foxx.androidcast.sender.SenderResolutionUiMode;

/**
 * Global app preferences: identity, PIN, tray icon, and default sender/receiver cast options.
 */
public final class AppPreferences {
    private static final String PREFS = "androidcast_app";

    private static final String KEY_USERNAME = "username";
    private static final String KEY_PIN = "pin";
    private static final String KEY_SHOW_TRAY = "show_tray_icon";
    private static final String KEY_THEME = "theme_mode";
    private static final String KEY_LOCALE = "locale_mode";
    private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio";
    private static final String KEY_DEV_RECEIVER_DEBUG_OVERLAY = "dev_receiver_debug_overlay";
    private static final String KEY_DEV_GRAB_SESSION_STATS = "dev_grab_session_stats";
    private static final String KEY_DEV_STREAM_DUMP = "dev_stream_dump";
    private static final String KEY_SEND_ANONYMOUS_CRASH_LOGS = "send_anonymous_crash_logs";
    private static final String KEY_DEV_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores";
    private static final String KEY_DEV_RECEIVER_VIDEO_PRESET = "dev_receiver_video_preset";
    private static final String KEY_DEV_RECEIVER_AUDIO_PRESET = "dev_receiver_audio_preset";
    private static final String KEY_DEV_RECEIVER_AV_INTENSITY = "dev_receiver_av_intensity";
    private static final String KEY_DEV_RECEIVER_ADAPTIVE_DISPLAY = "dev_receiver_adaptive_display";
    private static final String KEY_RECEIVER_PIP_USER_DEFINED = "receiver_pip_user_defined";
    private static final String KEY_RECEIVER_PIP_ENABLED = "receiver_pip_enabled";
    private static final String KEY_OTA_CHANNEL_URL = "ota_channel_url";
    private static final String KEY_OTA_MANIFEST_URL_LEGACY = "ota_manifest_url";
    private static final String KEY_OTA_CHECK_ON_LAUNCH = "ota_check_on_launch";
    private static final String KEY_OTA_LAST_CHECK_MS = "ota_last_check_ms";
    private static final String KEY_OTA_DISMISSED_VERSION = "ota_dismissed_version_code";
    private static final String KEY_OTA_ROUND_ROBIN_INDEX = "ota_round_robin_index";

    private static final String KEY_DEV_WIRED_DISPLAY_HINTS = "dev_wired_display_hints";
    private static final String KEY_DEV_USB_TETHER_TRANSPORT = "dev_usb_tether_transport";
    private static final String KEY_DEV_SENDER_RESOLUTION_UI_MODE = "dev_sender_resolution_ui_mode";
    private static final String KEY_DEV_EXTERNAL_CAPTURE_POLICY = "dev_external_capture_policy";
    private static final String KEY_DEV_COMMERCIAL_ADS = "dev_commercial_ads";
    private static final String KEY_DEV_COMMERCIAL_IAP = "dev_commercial_iap";
    private static final String KEY_DEV_COMMERCIAL_PLAY_STORE = "dev_commercial_play_store";
    private static final String KEY_DEV_DIAG_PING_PONG = "dev_diag_ping_pong";
    private static final String KEY_DEV_DIAG_PING_RESPONSE_MODE = "dev_diag_ping_response_mode";
    private static final String KEY_DEV_BACKEND_NTP_SYNC = "dev_backend_ntp_sync";
    private static final String KEY_DEV_BACKEND_NTP_OFFSET_MS = "dev_backend_ntp_offset_ms";
    private static final String KEY_DEV_BACKEND_NTP_LAST_SYNC_MS = "dev_backend_ntp_last_sync_ms";
    private static final String KEY_DEV_BACKEND_NTP_LAST_SKEW_MS = "dev_backend_ntp_last_skew_ms";
    private static final String KEY_DEV_REMOTE_ACCESS_MODE = "dev_remote_access_mode";
    private static final String KEY_DEV_REMOTE_ACCESS_RANDOM = "dev_remote_access_random";
    private static final String KEY_DEV_REMOTE_ACCESS_SESSION_ID = "dev_remote_access_session_id";
    private static final String KEY_DEV_REMOTE_ACCESS_EXPIRES_AT = "dev_remote_access_expires_at";
    private static final String KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE = "dev_remote_access_vpn_route_scope";
    private static final String KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE = "dev_remote_access_vpn_app_scope";
    private static final String KEY_DEV_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper";
    private static final String KEY_ENTITLEMENT_ACCOUNT_KEY = "entitlement_account_key";
    private static final String KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS = "entitlement_last_verified_wall_ms";

    /** Minimum interval between automatic OTA checks on app launch. */
    public static final long OTA_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000L;

    private static final String PREFIX_SENDER = "sender_";
    private static final String PREFIX_RECEIVER = "receiver_";

    private AppPreferences() {}

    public static String getUsername(Context context) {
        String stored = prefs(context).getString(KEY_USERNAME, null);
        if (!TextUtils.isEmpty(stored)) {
            return stored.trim();
        }
        return defaultDeviceName(context);
    }

    public static void setUsername(Context context, String username) {
        String v = username != null ? username.trim() : "";
        if (v.isEmpty()) {
            v = defaultDeviceName(context);
        }
        prefs(context).edit().putString(KEY_USERNAME, v).apply();
    }

    public static String getPin(Context context) {
        return prefs(context).getString(KEY_PIN, CastConfig.DEFAULT_PIN);
    }

    public static void setPin(Context context, String pin) {
        prefs(context).edit().putString(KEY_PIN, pin != null ? pin : "").apply();
    }

    public static boolean isShowTrayIcon(Context context) {
        return prefs(context).getBoolean(KEY_SHOW_TRAY, true);
    }

    public static void setShowTrayIcon(Context context, boolean show) {
        prefs(context).edit().putBoolean(KEY_SHOW_TRAY, show).apply();
    }

    public static CastThemeHelper.AppThemeMode getThemeMode(Context context) {
        String name = prefs(context).getString(KEY_THEME, CastThemeHelper.AppThemeMode.SYSTEM.name());
        try {
            return CastThemeHelper.AppThemeMode.valueOf(name);
        } catch (Exception e) {
            return CastThemeHelper.AppThemeMode.SYSTEM;
        }
    }

    public static void setThemeMode(Context context, CastThemeHelper.AppThemeMode mode) {
        prefs(context).edit().putString(KEY_THEME,
                mode != null ? mode.name() : CastThemeHelper.AppThemeMode.SYSTEM.name()).apply();
    }

    public static CastLocaleHelper.AppLocaleMode getLocaleMode(Context context) {
        String name = prefs(context).getString(KEY_LOCALE, CastLocaleHelper.AppLocaleMode.SYSTEM.name());
        try {
            return CastLocaleHelper.AppLocaleMode.valueOf(name);
        } catch (Exception e) {
            return CastLocaleHelper.AppLocaleMode.SYSTEM;
        }
    }

    public static void setLocaleMode(Context context, CastLocaleHelper.AppLocaleMode mode) {
        prefs(context).edit().putString(KEY_LOCALE,
                mode != null ? mode.name() : CastLocaleHelper.AppLocaleMode.SYSTEM.name()).apply();
    }

    public static boolean isPlayIncomingAudio(Context context) {
        return prefs(context).getBoolean(KEY_PLAY_INCOMING_AUDIO, true);
    }

    public static void setPlayIncomingAudio(Context context, boolean play) {
        prefs(context).edit().putBoolean(KEY_PLAY_INCOMING_AUDIO, play).apply();
    }

    /** Developer: metrics HTML overlay on receiver video (default on). */
    public static boolean isShowReceiverDebugOverlay(Context context) {
        return prefs(context).getBoolean(KEY_DEV_RECEIVER_DEBUG_OVERLAY, true);
    }

    public static void setShowReceiverDebugOverlay(Context context, boolean show) {
        prefs(context).edit().putBoolean(KEY_DEV_RECEIVER_DEBUG_OVERLAY, show).apply();
    }

    /** Developer: write anonymous session_send_* / session_recv_* JSON (default on). */
    public static boolean isGrabSessionStats(Context context) {
        return prefs(context).getBoolean(KEY_DEV_GRAB_SESSION_STATS, true);
    }

    public static void setGrabSessionStats(Context context, boolean grab) {
        prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply();
    }

    /** Developer: record elementary cast streams to zip (FR 0.1; default off). */
    public static boolean isDevStreamDumpEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_STREAM_DUMP, false);
    }

    public static void setDevStreamDumpEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_STREAM_DUMP, enabled).apply();
    }

    /** Send anonymous crash reports via the standalone crash watcher process (default on). */
    public static boolean isSendAnonymousCrashLogs(Context context) {
        return prefs(context).getBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true);
    }

    public static void setSendAnonymousCrashLogs(Context context, boolean send) {
        prefs(context).edit().putBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, send).apply();
    }

    /** Developer: show priority scores in codec settings spinners (default off). */
    public static boolean isShowCodecPriorityScores(Context context) {
        return prefs(context).getBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, false);
    }

    public static void setShowCodecPriorityScores(Context context, boolean show) {
        prefs(context).edit().putBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, show).apply();
    }

    /** v0 channel URL ({@code …/v0/ota/channel/stable.json}) or direct manifest URL. */
    public static String getOtaChannelUrl(Context context) {
        SharedPreferences p = prefs(context);
        String url = p.getString(KEY_OTA_CHANNEL_URL, "").trim();
        if (url.isEmpty()) {
            url = p.getString(KEY_OTA_MANIFEST_URL_LEGACY, "").trim();
        }
        return url;
    }

    public static void setOtaChannelUrl(Context context, String url) {
        String v = url != null ? url.trim() : "";
        prefs(context).edit()
                .putString(KEY_OTA_CHANNEL_URL, v)
                .remove(KEY_OTA_MANIFEST_URL_LEGACY)
                .apply();
    }

    public static boolean isOtaCheckOnLaunch(Context context) {
        return prefs(context).getBoolean(KEY_OTA_CHECK_ON_LAUNCH, true);
    }

    public static void setOtaCheckOnLaunch(Context context, boolean check) {
        prefs(context).edit().putBoolean(KEY_OTA_CHECK_ON_LAUNCH, check).apply();
    }

    public static long getOtaLastCheckMs(Context context) {
        return prefs(context).getLong(KEY_OTA_LAST_CHECK_MS, 0L);
    }

    public static void setOtaLastCheckMs(Context context, long epochMs) {
        prefs(context).edit().putLong(KEY_OTA_LAST_CHECK_MS, epochMs).apply();
    }

    public static int getOtaRoundRobinIndex(Context context) {
        return Math.max(0, prefs(context).getInt(KEY_OTA_ROUND_ROBIN_INDEX, 0));
    }

    public static void setOtaRoundRobinIndex(Context context, int nextIndex) {
        prefs(context).edit().putInt(KEY_OTA_ROUND_ROBIN_INDEX, Math.max(0, nextIndex)).apply();
    }

    public static int getOtaDismissedVersionCode(Context context) {
        return prefs(context).getInt(KEY_OTA_DISMISSED_VERSION, 0);
    }

    public static void setOtaDismissedVersionCode(Context context, int versionCode) {
        prefs(context).edit().putInt(KEY_OTA_DISMISSED_VERSION, versionCode).apply();
    }

    /**
     * Receiver PiP: user toggle when set; otherwise derived from PiP support + notifications permission.
     */
    public static boolean isReceiverPipEnabled(Context context) {
        if (prefs(context).getBoolean(KEY_RECEIVER_PIP_USER_DEFINED, false)) {
            return prefs(context).getBoolean(KEY_RECEIVER_PIP_ENABLED, false);
        }
        return defaultReceiverPipEnabled(context);
    }

    /** Value shown in settings before the user changes the toggle. */
    public static boolean getReceiverPipEffective(Context context) {
        return isReceiverPipEnabled(context);
    }

    public static boolean isReceiverPipUserDefined(Context context) {
        return prefs(context).getBoolean(KEY_RECEIVER_PIP_USER_DEFINED, false);
    }

    public static void setReceiverPipUserChoice(Context context, boolean enabled) {
        prefs(context).edit()
                .putBoolean(KEY_RECEIVER_PIP_USER_DEFINED, true)
                .putBoolean(KEY_RECEIVER_PIP_ENABLED, enabled)
                .apply();
    }

    private static boolean defaultReceiverPipEnabled(Context context) {
        return PictureInPictureHelper.isSupported()
                && PictureInPictureHelper.isAllowedForApp(context)
                && PermissionHelper.hasReceiverPlaybackReady(context);
    }

    public static CastSettings loadSenderDefaults(Context context) {
        CastSettings s = new CastSettings();
        applyStoredCastSettings(context, PREFIX_SENDER, s);
        s.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
        s.setQuality(CastSettings.Quality.MEDIUM);
        if (s.getBitrateMode() == CastSettings.BitrateMode.AUTO) {
            s.setBitrateMode(CastSettings.BitrateMode.ESTIMATED);
        }
        if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
            s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
        }
        clampAlphaTransport(s);
        return s;
    }

    public static void saveSenderDefaults(Context context, CastSettings settings) {
        if (settings == null) {
            return;
        }
        storeCastSettings(context, PREFIX_SENDER, settings);
    }

    public static ReceiverVideoPreset getReceiverVideoPreset(Context context) {
        return ReceiverVideoPreset.fromName(
                prefs(context).getString(KEY_DEV_RECEIVER_VIDEO_PRESET, null),
                ReceiverVideoPreset.DEFAULT);
    }

    public static void setReceiverVideoPreset(Context context, ReceiverVideoPreset preset) {
        prefs(context).edit()
                .putString(KEY_DEV_RECEIVER_VIDEO_PRESET, preset.name())
                .apply();
    }

    public static ReceiverAudioPreset getReceiverAudioPreset(Context context) {
        return ReceiverAudioPreset.fromName(
                prefs(context).getString(KEY_DEV_RECEIVER_AUDIO_PRESET, null),
                ReceiverAudioPreset.DEFAULT);
    }

    public static void setReceiverAudioPreset(Context context, ReceiverAudioPreset preset) {
        prefs(context).edit()
                .putString(KEY_DEV_RECEIVER_AUDIO_PRESET, preset.name())
                .apply();
    }

    public static int getReceiverAvIntensity(Context context) {
        return Math.max(0, Math.min(10, prefs(context).getInt(KEY_DEV_RECEIVER_AV_INTENSITY, 5)));
    }

    public static void setReceiverAvIntensity(Context context, int intensity) {
        prefs(context).edit()
                .putInt(KEY_DEV_RECEIVER_AV_INTENSITY, Math.max(0, Math.min(10, intensity)))
                .apply();
    }

    public static boolean isDevReceiverAdaptiveDisplay(Context context) {
        return prefs(context).getBoolean(KEY_DEV_RECEIVER_ADAPTIVE_DISPLAY, false);
    }

    public static void setDevReceiverAdaptiveDisplay(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_RECEIVER_ADAPTIVE_DISPLAY, enabled).apply();
    }

    public static boolean isDevWiredDisplayHintsEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_WIRED_DISPLAY_HINTS, false);
    }

    public static void setDevWiredDisplayHintsEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_WIRED_DISPLAY_HINTS, enabled).apply();
    }

    public static boolean isDevUsbTetherTransportEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_USB_TETHER_TRANSPORT, false);
    }

    public static void setDevUsbTetherTransportEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_USB_TETHER_TRANSPORT, enabled).apply();
    }

    /** Default: {@link SenderResolutionUiMode#SEPARATED}. */
    public static SenderResolutionUiMode getDevSenderResolutionUiMode(Context context) {
        String name = prefs(context).getString(
                KEY_DEV_SENDER_RESOLUTION_UI_MODE, SenderResolutionUiMode.SEPARATED.name());
        try {
            return SenderResolutionUiMode.valueOf(name);
        } catch (IllegalArgumentException e) {
            return SenderResolutionUiMode.SEPARATED;
        }
    }

    public static void setDevSenderResolutionUiMode(Context context, SenderResolutionUiMode mode) {
        SenderResolutionUiMode value = mode != null ? mode : SenderResolutionUiMode.SEPARATED;
        prefs(context).edit().putString(KEY_DEV_SENDER_RESOLUTION_UI_MODE, value.name()).apply();
    }

    public static ExternalDisplayCapturePolicy getExternalDisplayCapturePolicy(Context context) {
        return ExternalDisplayCapturePolicy.fromName(
                prefs(context).getString(KEY_DEV_EXTERNAL_CAPTURE_POLICY, null),
                ExternalDisplayCapturePolicy.DEFAULT_DISPLAY_ONLY);
    }

    public static void setExternalDisplayCapturePolicy(Context context, ExternalDisplayCapturePolicy policy) {
        prefs(context).edit()
                .putString(KEY_DEV_EXTERNAL_CAPTURE_POLICY,
                        policy != null ? policy.name() : ExternalDisplayCapturePolicy.DEFAULT_DISPLAY_ONLY.name())
                .apply();
    }

    public static boolean isDevCommercialAdsEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_COMMERCIAL_ADS, false);
    }

    public static void setDevCommercialAdsEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_COMMERCIAL_ADS, enabled).apply();
    }

    public static boolean isDevCommercialIapEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_COMMERCIAL_IAP, false);
    }

    public static void setDevCommercialIapEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_COMMERCIAL_IAP, enabled).apply();
    }

    public static boolean isDevCommercialPlayStoreEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_COMMERCIAL_PLAY_STORE, false);
    }

    public static void setDevCommercialPlayStoreEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_COMMERCIAL_PLAY_STORE, enabled).apply();
    }

    /** Developer: ~1 Hz diagnostic cast ping-pong during sessions (default off). */
    public static boolean isDevDiagPingPongEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_DIAG_PING_PONG, false);
    }

    public static void setDevDiagPingPongEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_DIAG_PING_PONG, enabled).apply();
    }

    /**
     * How this device responds to peer diag probes:
     * 0 mirror-as-is, 1 mirror-smart (default), 2 reply-with.
     */
    public static byte getDevDiagPingResponseMode(Context context) {
        int mode = prefs(context).getInt(KEY_DEV_DIAG_PING_RESPONSE_MODE,
                com.foxx.androidcast.network.diag.CastDiagPing.DIR_MIRROR_SMART);
        if (mode < 0 || mode > 2) {
            return com.foxx.androidcast.network.diag.CastDiagPing.DIR_MIRROR_SMART;
        }
        return (byte) mode;
    }

    public static void setDevDiagPingResponseMode(Context context, byte mode) {
        int v = mode;
        if (v < 0 || v > 2) {
            v = com.foxx.androidcast.network.diag.CastDiagPing.DIR_MIRROR_SMART;
        }
        prefs(context).edit().putInt(KEY_DEV_DIAG_PING_RESPONSE_MODE, v).apply();
    }

    /** Developer: allow backend-provided NTP correction for diagnostics (default off). */
    public static boolean isDevBackendNtpSyncEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_BACKEND_NTP_SYNC, false);
    }

    public static void setDevBackendNtpSyncEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_BACKEND_NTP_SYNC, enabled).apply();
        if (!enabled) {
            clearDevBackendNtpOffset(context);
        }
    }

    public static long getDevBackendNtpOffsetMs(Context context) {
        return prefs(context).getLong(KEY_DEV_BACKEND_NTP_OFFSET_MS, 0L);
    }

    public static void setDevBackendNtpOffsetMs(Context context, long offsetMs) {
        prefs(context).edit().putLong(KEY_DEV_BACKEND_NTP_OFFSET_MS, offsetMs).apply();
    }

    public static long getDevBackendNtpLastSyncMs(Context context) {
        return prefs(context).getLong(KEY_DEV_BACKEND_NTP_LAST_SYNC_MS, 0L);
    }

    public static void setDevBackendNtpLastSyncMs(Context context, long whenMs) {
        prefs(context).edit().putLong(KEY_DEV_BACKEND_NTP_LAST_SYNC_MS, whenMs).apply();
    }

    public static long getDevBackendNtpLastSkewMs(Context context) {
        return prefs(context).getLong(KEY_DEV_BACKEND_NTP_LAST_SKEW_MS, 0L);
    }

    public static void setDevBackendNtpLastSkewMs(Context context, long skewMs) {
        prefs(context).edit().putLong(KEY_DEV_BACKEND_NTP_LAST_SKEW_MS, skewMs).apply();
    }

    public static void clearDevBackendNtpOffset(Context context) {
        prefs(context).edit()
                .remove(KEY_DEV_BACKEND_NTP_OFFSET_MS)
                .remove(KEY_DEV_BACKEND_NTP_LAST_SYNC_MS)
                .remove(KEY_DEV_BACKEND_NTP_LAST_SKEW_MS)
                .apply();
    }

    /** Debug builds: auto re-enable wireless adb after Wi-Fi reconnects. */
    public static boolean isDevAdbWifiKeeperEnabled(Context context) {
        return prefs(context).getBoolean(KEY_DEV_ADB_WIFI_KEEPER, true);
    }

    public static void setDevAdbWifiKeeperEnabled(Context context, boolean enabled) {
        prefs(context).edit().putBoolean(KEY_DEV_ADB_WIFI_KEEPER, enabled).apply();
    }

    public static com.foxx.androidcast.remoteaccess.RemoteAccessMode getDevRemoteAccessMode(Context context) {
        return com.foxx.androidcast.remoteaccess.RemoteAccessMode.fromStored(
                prefs(context).getString(KEY_DEV_REMOTE_ACCESS_MODE, com.foxx.androidcast.remoteaccess.RemoteAccessMode.DISABLED.name()));
    }

    public static void setDevRemoteAccessMode(Context context, com.foxx.androidcast.remoteaccess.RemoteAccessMode mode) {
        if (mode == null) {
            mode = com.foxx.androidcast.remoteaccess.RemoteAccessMode.DISABLED;
        }
        prefs(context).edit().putString(KEY_DEV_REMOTE_ACCESS_MODE, mode.name()).apply();
    }

    public static String getDevRemoteAccessRandom(Context context) {
        return prefs(context).getString(KEY_DEV_REMOTE_ACCESS_RANDOM, "");
    }

    public static void setDevRemoteAccessRandom(Context context, String random) {
        prefs(context).edit().putString(KEY_DEV_REMOTE_ACCESS_RANDOM, random != null ? random : "").apply();
    }

    public static void setDevRemoteAccessSession(Context context, String sessionId, long expiresAtEpochSec) {
        prefs(context).edit()
                .putString(KEY_DEV_REMOTE_ACCESS_SESSION_ID, sessionId != null ? sessionId : "")
                .putLong(KEY_DEV_REMOTE_ACCESS_EXPIRES_AT, expiresAtEpochSec)
                .apply();
    }

    public static void clearDevRemoteAccessSession(Context context) {
        prefs(context).edit()
                .remove(KEY_DEV_REMOTE_ACCESS_SESSION_ID)
                .remove(KEY_DEV_REMOTE_ACCESS_EXPIRES_AT)
                .apply();
    }

    public static long getDevRemoteAccessExpiresAt(Context context) {
        return prefs(context).getLong(KEY_DEV_REMOTE_ACCESS_EXPIRES_AT, 0L);
    }

    public static com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.RouteScope getDevRemoteAccessVpnRouteScope(
            Context context) {
        return com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.RouteScope.fromStored(
                prefs(context).getString(
                        KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE,
                        com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.RouteScope.HUB_ONLY.name()));
    }

    public static void setDevRemoteAccessVpnRouteScope(
            Context context,
            com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.RouteScope scope) {
        if (scope == null) {
            scope = com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.RouteScope.HUB_ONLY;
        }
        prefs(context).edit().putString(KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE, scope.name()).apply();
    }

    public static com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.AppScope getDevRemoteAccessVpnAppScope(
            Context context) {
        return com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.AppScope.fromStored(
                prefs(context).getString(
                        KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE,
                        com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.AppScope.ALL_APPS.name()));
    }

    public static void setDevRemoteAccessVpnAppScope(
            Context context,
            com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.AppScope scope) {
        if (scope == null) {
            scope = com.foxx.androidcast.remoteaccess.RemoteAccessVpnRouting.AppScope.ALL_APPS;
        }
        prefs(context).edit().putString(KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE, scope.name()).apply();
    }

    /**
     * Reset user-facing settings shown in Android Cast settings.
     * Developer-only settings are intentionally preserved.
     */
    public static void resetUserVisibleSettings(Context context) {
        SharedPreferences p = prefs(context);
        SharedPreferences.Editor e = p.edit();

        e.putString(KEY_USERNAME, defaultDeviceName(context));
        e.putString(KEY_PIN, CastConfig.DEFAULT_PIN);
        e.putBoolean(KEY_SHOW_TRAY, true);
        e.putString(KEY_THEME, CastThemeHelper.AppThemeMode.SYSTEM.name());
        e.putString(KEY_LOCALE, CastLocaleHelper.AppLocaleMode.SYSTEM.name());
        e.putBoolean(KEY_PLAY_INCOMING_AUDIO, true);
        e.putBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true);
        e.putBoolean(KEY_RECEIVER_PIP_USER_DEFINED, false);
        e.putBoolean(KEY_RECEIVER_PIP_ENABLED, false);

        java.util.Map<String, ?> all = p.getAll();
        for (String key : all.keySet()) {
            if (key.startsWith(PREFIX_SENDER) || key.startsWith(PREFIX_RECEIVER)) {
                e.remove(key);
            }
        }
        e.apply();
    }

    public static CastSettings loadReceiverDefaults(Context context) {
        CastSettings s = new CastSettings();
        applyStoredCastSettings(context, PREFIX_RECEIVER, s);
        if (isDevReceiverAdaptiveDisplay(context)) {
            s.setReceiverDisplayResolution(CastSettings.ReceiverDisplayResolution.ADAPTIVE_EXPERIMENTAL);
        } else {
            s.setReceiverDisplayResolution(CastSettings.ReceiverDisplayResolution.AUTO_FIT);
        }
        s.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
        s.setQuality(CastSettings.Quality.MEDIUM);
        if (s.getBitrateMode() == CastSettings.BitrateMode.AUTO) {
            s.setBitrateMode(CastSettings.BitrateMode.ESTIMATED);
        }
        if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
            s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
        }
        clampAlphaTransport(s);
        return s;
    }

    public static void saveReceiverDefaults(Context context, CastSettings settings) {
        if (settings == null) {
            return;
        }
        storeCastSettings(context, PREFIX_RECEIVER, settings);
    }

    public static String defaultDeviceName(Context context) {
        String name = Settings.Global.getString(context.getContentResolver(), Settings.Global.DEVICE_NAME);
        if (TextUtils.isEmpty(name)) {
            name = Build.MODEL;
        }
        return name != null ? name : "Android device";
    }

    private static void applyStoredCastSettings(Context context, String prefix, CastSettings s) {
        SharedPreferences p = prefs(context);
        String transport = p.getString(prefix + "transport", CastConfig.DEFAULT_TRANSPORT);
        s.setTransport(transport);
        CastSettings.Resolution resolution = readEnum(p, prefix + "resolution",
                CastSettings.Resolution.values(), CastSettings.Resolution.ADAPTIVE);
        if (PREFIX_SENDER.equals(prefix)) {
            resolution = CastSettings.forPresetUi(resolution);
        }
        s.setResolution(resolution);
        s.setVideoCodec(readEnum(p, prefix + "video_codec", CastSettings.VideoCodec.values(),
                CastSettings.VideoCodec.AUTO));
        s.setBitrateMode(readEnum(p, prefix + "bitrate_mode", CastSettings.BitrateMode.values(),
                CastSettings.BitrateMode.ESTIMATED));
        s.setNetworkAdoption(readEnum(p, prefix + "network_adoption", CastSettings.NetworkAdoption.values(),
                CastSettings.NetworkAdoption.ESTIMATED));
        s.setCaptureMode(readEnum(p, prefix + "capture_mode", CastSettings.CaptureMode.values(),
                PlatformCastSupport.defaultCaptureMode()));
        CastSettings.StreamProtection protection = readEnum(p, prefix + "stream_protection",
                CastSettings.StreamProtection.values(), CastSettings.StreamProtection.NONE);
        if (protection == CastSettings.StreamProtection.AUTO) {
            protection = CastSettings.StreamProtection.NONE;
        }
        s.setStreamProtection(protection);
        s.setReceiverDisplayResolution(readEnum(p, prefix + "receiver_display",
                CastSettings.ReceiverDisplayResolution.values(),
                CastSettings.ReceiverDisplayResolution.AUTO_FIT));
        s.setAudioEnabled(p.getBoolean(prefix + "audio_enabled", PlatformCastSupport.isAudioCaptureSupported()));
        s.setAudioCodec(readEnum(p, prefix + "audio_codec", CastSettings.AudioCodec.values(),
                CastSettings.AudioCodec.AUTO));
    }

    private static void storeCastSettings(Context context, String prefix, CastSettings s) {
        prefs(context).edit()
                .putString(prefix + "transport", s.getTransport())
                .putString(prefix + "resolution", s.getResolution().name())
                .putString(prefix + "video_codec", s.getVideoCodec().name())
                .putString(prefix + "bitrate_mode", s.getBitrateMode().name())
                .putString(prefix + "network_adoption", s.getNetworkAdoption().name())
                .putString(prefix + "capture_mode", s.getCaptureMode().name())
                .putString(prefix + "stream_protection", s.getStreamProtection().name())
                .putString(prefix + "receiver_display", s.getReceiverDisplayResolution().name())
                .putBoolean(prefix + "audio_enabled", s.isAudioRequested())
                .putString(prefix + "audio_codec", s.getAudioCodec().name())
                .apply();
    }

    private static <T extends Enum<T>> T readEnum(SharedPreferences p, String key, T[] values, T fallback) {
        String name = p.getString(key, fallback.name());
        for (T v : values) {
            if (v.name().equals(name)) {
                return v;
            }
        }
        return fallback;
    }

    private static void clampAlphaTransport(CastSettings settings) {
        if (settings == null || !CastConfig.ALPHA_FEATURE_FREEZE) {
            return;
        }
        if (!CastConfig.isReleaseTransport(settings.getTransport())) {
            settings.setTransport(CastConfig.DEFAULT_TRANSPORT);
        }
    }

    public static String getEntitlementAccountKey(Context context) {
        return prefs(context).getString(KEY_ENTITLEMENT_ACCOUNT_KEY, "");
    }

    public static void setEntitlementAccountKey(Context context, String keyHex) {
        prefs(context).edit().putString(KEY_ENTITLEMENT_ACCOUNT_KEY,
                keyHex != null ? keyHex : "").apply();
    }

    public static long getEntitlementLastVerifiedWallMs(Context context) {
        return prefs(context).getLong(KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS, 0L);
    }

    public static void setEntitlementLastVerifiedWallMs(Context context, long wallMs) {
        prefs(context).edit().putLong(KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS, wallMs).apply();
    }

    private static SharedPreferences prefs(Context context) {
        if (context == null) {
            throw new IllegalStateException("context is null");
        }
        Context storage = context;
        Context app = context.getApplicationContext();
        if (app != null) {
            storage = app;
        }
        return storage.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
    }
}
