using UnityEngine;
using GeoSus.Client;
using System;
using System.Collections;

namespace Subsystems
{
    internal class CoroutineHost : MonoBehaviour
    {
        public CoroutineHost() { }
    }
    internal enum GPSState
    {
        Uninitialized,
        Initializing,
        Running,
        Failed
    }

    /// <summary>
    /// Position source backend. Selectable at runtime via the GPS overlay
    /// "Source" button so the user can recover when one path misbehaves on
    /// their phone:
    ///   Auto         - JNI: subscribe to gps + network, pick most recent fix.
    ///   GpsOnly      - JNI: subscribe to gps only (network's frequent indoor
    ///                  fixes don't drown out the slower-but-precise gps fix).
    ///   NetworkOnly  - JNI: subscribe to network only (cell tower / WiFi).
    ///                  Useful indoors when no satellite lock is possible.
    ///   UnityInput   - Unity's Input.location wrapper. Verified to hang on
    ///                  Mi 9T / A20e (which is why JNI exists), but works on
    ///                  newer Android where the JNI streaming-callbacks path
    ///                  silently doesn't fire (MIUI/HyperOS battery saver,
    ///                  approximate-vs-precise permission split, minDistance
    ///                  gating on stationary phones).
    ///   EditorWasd   - WASD-driven simulated position. Available regardless
    ///                  of testMode flag so desktop builds and editor sessions
    ///                  can navigate the map without real GPS.
    /// </summary>
    public enum PositionSource
    {
        Auto,
        GpsOnly,
        NetworkOnly,
        UnityInput,
        EditorWasd,
    }

#if UNITY_ANDROID && !UNITY_EDITOR
    /// <summary>
    /// Bridges android.location.LocationListener to managed code. The method
    /// names here must match Java's LocationListener interface exactly so
    /// AndroidJavaProxy's reflection dispatcher can find them.
    /// </summary>
    internal class AndroidLocationProxy : AndroidJavaProxy
    {
        public AndroidLocationProvider Owner { get; set; }
        public AndroidLocationProxy() : base("android.location.LocationListener") { }

        // Called by Android each time a new fix arrives from the registered provider.
        public void onLocationChanged(AndroidJavaObject location)
        {
            try
            {
                if (location == null) return;
                double lat = location.Call<double>("getLatitude");
                double lon = location.Call<double>("getLongitude");
                long t = location.Call<long>("getTime");
                string provider = "";
                try { provider = location.Call<string>("getProvider"); } catch { }
                // Streaming callbacks are LIVE (never cached). The cached path
                // calls UpdateLocation directly with isCached=true.
                Owner?.UpdateLocation(lat, lon, t, provider, isCached: false);
            }
            catch (Exception ex)
            {
                Debug.LogWarning("[GPS-JNI] onLocationChanged failed: " + ex.Message);
            }
        }

        // Required by the LocationListener interface even if we don't use them.
        // Missing methods cause java.lang.AbstractMethodError at runtime.
        public void onStatusChanged(string provider, int status, AndroidJavaObject extras) { }
        public void onProviderEnabled(string provider) { }
        public void onProviderDisabled(string provider) { }
    }

    /// <summary>
    /// Direct wrapper around android.location.LocationManager via JNI, used as
    /// a replacement for Unity's Input.location on Android when the user picks
    /// Auto/GpsOnly/NetworkOnly. Subscribed providers are configurable so the
    /// position-source picker can rewire live without restart.
    /// </summary>
    internal class AndroidLocationProvider
    {
        private AndroidJavaObject _activity;
        private AndroidJavaObject _locationManager;
        private AndroidLocationProxy _gpsListener;
        private AndroidLocationProxy _networkListener;
        private double _lat, _lon;
        private long _lastTimeMillis;
        private long _lastLiveTimeMillis;   // Time of most recent NON-cached fix.
        private bool _hasFix;
        private bool _hasLiveFix;           // True once any streaming callback fired.
        private string _activeProvider = "";

        // Captured at Initialize() so the diagnostic can report
        // "GPS provider DISABLED, only network enabled" etc.
        private bool _gpsProviderEnabled;
        private bool _networkProviderEnabled;
        private bool _gpsLastKnownExists;
        private bool _networkLastKnownExists;
        private string _enabledProvidersList = "";

        // Subscription scope - set in Initialize, used in Shutdown to know
        // which listeners we registered.
        private bool _subscribedGps;
        private bool _subscribedNetwork;

        public bool HasFix => _hasFix;
        public bool HasLiveFix => _hasLiveFix;
        public long LastLiveTimeMillis => _lastLiveTimeMillis;
        public long LastTimeMillis => _lastTimeMillis;
        public double Lat => _lat;
        public double Lon => _lon;
        public string ActiveProvider => _activeProvider;
        public bool GpsProviderEnabled => _gpsProviderEnabled;
        public bool NetworkProviderEnabled => _networkProviderEnabled;
        public bool GpsLastKnownExists => _gpsLastKnownExists;
        public bool NetworkLastKnownExists => _networkLastKnownExists;
        public string EnabledProvidersList => _enabledProvidersList;
        public bool SubscribedGps => _subscribedGps;
        public bool SubscribedNetwork => _subscribedNetwork;

        public bool Initialize(out string error, bool useGps, bool useNetwork)
        {
            error = "";
            try
            {
                using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
                {
                    _activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
                }
                if (_activity == null) { error = "no current activity"; return false; }

                _locationManager = _activity.Call<AndroidJavaObject>("getSystemService", "location");
                if (_locationManager == null) { error = "getSystemService(\"location\") returned null"; return false; }

                // Capture provider enable state up front so the diagnostic
                // can distinguish "provider disabled at OS level" from
                // "provider enabled but produced no fix yet".
                _gpsProviderEnabled = SafeIsProviderEnabled("gps");
                _networkProviderEnabled = SafeIsProviderEnabled("network");
                _enabledProvidersList = SafeGetEnabledProviders();

                Debug.Log($"[GPS-JNI] init useGps={useGps} useNetwork={useNetwork} gps enabled={_gpsProviderEnabled} network enabled={_networkProviderEnabled} all enabled=[{_enabledProvidersList}]");

                // Try cached last-known fixes from the providers we're about
                // to subscribe to. If the OS already knows where we are
                // (e.g. from another app that recently used GPS), we get a
                // fix at zero cost and zero wait time. Tagged isCached so
                // the diagnostic can mark them and we know we still need
                // to wait for a streaming callback.
                if (useNetwork) TryLastKnown("network", out _networkLastKnownExists);
                if (useGps)     TryLastKnown("gps",     out _gpsLastKnownExists);

                _subscribedGps = useGps;
                _subscribedNetwork = useNetwork;

                if (useGps)     _gpsListener     = new AndroidLocationProxy { Owner = this };
                if (useNetwork) _networkListener = new AndroidLocationProxy { Owner = this };

                // requestLocationUpdates must be called on a thread with a
                // Looper. Use the Activity's UI thread, which always has one.
                // minTime=1000ms, minDistance=0f - we want updates on every
                // fix the OS produces. Previously this was 1f which gated
                // out updates from a stationary phone (MIUI/newer Android
                // are stricter about this and that's the suspected cause of
                // "via gps (cached)" sticking forever).
                _activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
                {
                    if (useGps)
                    {
                        try { _locationManager.Call("requestLocationUpdates", "gps", 1000L, 0f, _gpsListener); }
                        catch (Exception ex) { Debug.LogWarning("[GPS-JNI] gps subscribe failed: " + ex.Message); }
                    }
                    if (useNetwork)
                    {
                        try { _locationManager.Call("requestLocationUpdates", "network", 1000L, 0f, _networkListener); }
                        catch (Exception ex) { Debug.LogWarning("[GPS-JNI] network subscribe failed: " + ex.Message); }
                    }
                }));

                return true;
            }
            catch (Exception ex)
            {
                error = "JNI init exception: " + ex.Message;
                return false;
            }
        }

        void TryLastKnown(string provider, out bool nonNullReturned)
        {
            nonNullReturned = false;
            try
            {
                var loc = _locationManager.Call<AndroidJavaObject>("getLastKnownLocation", provider);
                if (loc != null)
                {
                    nonNullReturned = true;
                    double lat = loc.Call<double>("getLatitude");
                    double lon = loc.Call<double>("getLongitude");
                    long t = loc.Call<long>("getTime");
                    UpdateLocation(lat, lon, t, provider, isCached: true);
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[GPS-JNI] getLastKnownLocation({provider}) failed: " + ex.Message);
            }
        }

        bool SafeIsProviderEnabled(string provider)
        {
            try
            {
                return _locationManager.Call<bool>("isProviderEnabled", provider);
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[GPS-JNI] isProviderEnabled({provider}) failed: " + ex.Message);
                return false;
            }
        }

        // Build a comma-separated list of currently-enabled providers via
        // LocationManager.getProviders(true). We iterate the returned
        // java.util.List by index because AndroidJavaObject does not
        // implement IEnumerable.
        string SafeGetEnabledProviders()
        {
            try
            {
                var list = _locationManager.Call<AndroidJavaObject>("getProviders", true);
                if (list == null) return "";
                int size = list.Call<int>("size");
                var parts = new System.Text.StringBuilder();
                for (int i = 0; i < size; i++)
                {
                    var name = list.Call<string>("get", i);
                    if (i > 0) parts.Append(",");
                    parts.Append(name);
                }
                return parts.ToString();
            }
            catch (Exception ex)
            {
                Debug.LogWarning("[GPS-JNI] getProviders failed: " + ex.Message);
                return "";
            }
        }

        public void UpdateLocation(double lat, double lon, long timeMillis, string provider, bool isCached)
        {
            // Ignore older fixes if a newer one is already in hand. This lets
            // both gps + network listeners feed us without ping-ponging
            // between stale and fresh data.
            if (timeMillis < _lastTimeMillis) return;
            _lat = lat;
            _lon = lon;
            _lastTimeMillis = timeMillis;
            // Active-provider name carries cached/live state in the diagnostic
            // banner so the user can see at a glance whether streaming has
            // kicked in or we're still on the initial cached snapshot.
            _activeProvider = (provider ?? "") + (isCached ? " (cached)" : "");
            _hasFix = true;
            if (!isCached)
            {
                _hasLiveFix = true;
                _lastLiveTimeMillis = timeMillis;
            }
        }

        public void Shutdown()
        {
            try
            {
                if (_locationManager != null)
                {
                    if (_gpsListener != null) _locationManager.Call("removeUpdates", _gpsListener);
                    if (_networkListener != null) _locationManager.Call("removeUpdates", _networkListener);
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning("[GPS-JNI] Shutdown failed: " + ex.Message);
            }
            _gpsListener = null;
            _networkListener = null;
            _locationManager = null;
            _activity = null;
        }
    }
#endif
    public static class PositonExtensions
    {
        public static Position ToLocal(this Position position, Position center)
        {
            double latDiff = position.Lat - center.Lat;
            double lonDiff = position.Lon - center.Lon;
            double metersPerDegreeLat = 111320.0;
            double metersPerDegreeLon = 111320.0 * Math.Cos(center.Lat * Math.PI / 180.0);
            float x = (float)(lonDiff * metersPerDegreeLon);
            float z = (float)(latDiff * metersPerDegreeLat);
            return new Position(z, x);
        }
        public static Vector3 ToLocalVector3(this Position position, Position center)
        {
            return position.ToLocal(center).ToVector3(); //TODO: Implementace v subsystemech
        }
        public static Vector3 ToVector3(this Position position)
        {
            return new Vector3((float)position.Lon, 0, (float)position.Lat); //TODO: Implementace v subsystemech
        }
        public static double DistanceTo(this Vector3 pos, Vector3 other)
        {
            return Math.Sqrt((other.x - pos.x) * (other.x - pos.x) + (other.z - pos.z) * (other.z - pos.z));
        }

    }

    public class GameManager_Input
    {
        private GameClient _gameClient;
        private Position _currentPosition;
        private Position _lastSentPosition;
        private GameObject _player;
        private bool _testMode;

        // PlayerPrefs key for the user's chosen position source. Persists
        // across app restarts so a user who flipped to UnityInput because
        // their phone hated the JNI path doesn't have to flip again every
        // launch.
        private const string PrefsSourceKey = "PositionSource_v1";
        private PositionSource _currentSource = PositionSource.Auto;

        // When the multi-client editor test mode picks a non-host bot as
        // active, we need the host's WASD path to NOT also move. Set true
        // by GameManager when active slot != 0.
        public bool SuppressWasd = false;

        private GPSState _GPSState = GPSState.Uninitialized;
        private float _speed = 0.00001f;
        private Position _mapCenter;
        private CoroutineHost _coroutineHost;

        private int _gpsRetryCount = 0;
        private const int _maxGpsRetries = 5;
        private float _lastPositionSendTime;
        private const float _positionKeepAliveSeconds = 1.0f;

        // Diagnostic state. We capture *why* GPS init failed so the UI can
        // surface it to the user without requiring logcat. Older Android
        // phones (Mi 9T, A20e) hit silent failure modes that are impossible
        // to distinguish from "still warming up" without this.
        private string _lastGpsError = "";
        private float _gpsInitStartTime = -1f;
        // Bump from the original 20s. Cold-start GPS on older Android can
        // easily exceed 20s indoors or under cloud cover - by the time the
        // user notices nothing is happening, we've already given up.
        private const int _gpsInitTimeoutSeconds = 60;

#if UNITY_ANDROID && !UNITY_EDITOR
        // JNI-backed location provider, used for Auto/GpsOnly/NetworkOnly.
        // UnityInput uses Input.location instead and leaves this null.
        private AndroidLocationProvider _androidProvider;
#endif

        /// <summary>Last known GPS position (for CreateLobby centre point)</summary>
        public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null;

        /// <summary>Current GPS state machine value (debug/diagnostic).</summary>
        public string GpsStateName => _GPSState.ToString();

        /// <summary>Last GPS error reason captured during init (empty if none).</summary>
        public string LastGpsError => _lastGpsError ?? "";

        /// <summary>Retry count out of max (debug/diagnostic).</summary>
        public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}";

        /// <summary>Currently selected position source (for UI cycle button).</summary>
        public PositionSource CurrentSource => _currentSource;

        /// <summary>Display name for the current source (for UI label).</summary>
        public string CurrentSourceName
        {
            get
            {
                switch (_currentSource)
                {
                    case PositionSource.Auto:        return "Auto (GPS+Net)";
                    case PositionSource.GpsOnly:     return "GPS only";
                    case PositionSource.NetworkOnly: return "Network only";
                    case PositionSource.UnityInput:  return "Unity Input";
                    case PositionSource.EditorWasd:  return "WASD";
                    default: return _currentSource.ToString();
                }
            }
        }

        /// <summary>
        /// Human-readable one-line GPS status for on-screen overlay. Designed
        /// to be visible without ADB so users can self-diagnose permission
        /// vs. timeout vs. device-disabled vs. running-but-no-fix-yet.
        /// </summary>
        public string GpsDiagnostic
        {
            get
            {
                if (_currentSource == PositionSource.EditorWasd)
                {
                    if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
                        return "WASD: waiting for map center";
                    return $"WASD lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}";
                }

                switch (_GPSState)
                {
                    case GPSState.Uninitialized:
                        return "Uninitialized (will start on first lobby action)";
                    case GPSState.Initializing:
                    {
                        float elapsed = _gpsInitStartTime >= 0 ? Time.time - _gpsInitStartTime : 0;
                        string providers = "";
#if UNITY_ANDROID && !UNITY_EDITOR
                        if (_androidProvider != null && !string.IsNullOrEmpty(_androidProvider.EnabledProvidersList))
                            providers = $" providers=[{_androidProvider.EnabledProvidersList}]";
#endif
                        return $"Initializing ({elapsed:F1}s / max {_gpsInitTimeoutSeconds}s){providers}";
                    }
                    case GPSState.Running:
                    {
                        string suffix = "";
#if UNITY_ANDROID && !UNITY_EDITOR
                        if (_androidProvider != null)
                        {
                            string p = _androidProvider.ActiveProvider;
                            if (!string.IsNullOrEmpty(p)) suffix = " via " + p;
                            // Show how stale the most recent fix is (ms-level
                            // resolution) so "stuck on cached" is obvious at
                            // a glance: "via gps (cached) [no live, 47s old]".
                            if (!_androidProvider.HasLiveFix)
                            {
                                long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
                                long ageMs = now - _androidProvider.LastTimeMillis;
                                if (_androidProvider.LastTimeMillis > 0 && ageMs > 0)
                                    suffix += $" [no live, {ageMs / 1000}s old]";
                                else
                                    suffix += " [no live]";
                            }
                            else
                            {
                                long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
                                long ageMs = now - _androidProvider.LastLiveTimeMillis;
                                if (ageMs > 5000) suffix += $" [live {ageMs / 1000}s old]";
                            }
                        }
#endif
                        if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
                            return "Running but no fix yet (waiting for satellites)" + suffix;
                        return $"Running lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}" + suffix;
                    }
                    case GPSState.Failed:
                        return $"Failed: {(_lastGpsError ?? "unknown")} (retries {GpsRetryProgress})";
                    default:
                        return "?";
                }
            }
        }

        public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
        {
            _gameClient = gameClient;
            _player = player;
            _testMode = testMode;
            // CoroutineHost needs a MonoBehaviour on a real GameObject
            var hostGO = new UnityEngine.GameObject("_CoroutineHost");
            UnityEngine.Object.DontDestroyOnLoad(hostGO);
            _coroutineHost = hostGO.AddComponent<CoroutineHost>();

            // Restore the user's last picked source. Default depends on
            // platform: editor defaults to EditorWasd (no GPS hardware in
            // editor anyway); device defaults to Auto.
            string saved = PlayerPrefs.GetString(PrefsSourceKey, "");
            if (!string.IsNullOrEmpty(saved) && Enum.TryParse(saved, out PositionSource parsed))
            {
                _currentSource = parsed;
            }
            else
            {
#if UNITY_EDITOR
                _currentSource = PositionSource.EditorWasd;
#else
                _currentSource = PositionSource.Auto;
#endif
            }

            // Legacy testMode flag forces EditorWasd. New code paths should
            // use SwitchPositionSource(EditorWasd) instead, but we keep the
            // old behavior for backward compatibility with the inspector flag.
            if (_testMode) _currentSource = PositionSource.EditorWasd;
        }

        /// <summary>Called from OnSceneLoaded when Client.unity loads so the
        /// Player capsule (which lives in Client.unity) can be wired at runtime.</summary>
        public void SetPlayerObject(GameObject player) { _player = player; }

        /// <summary>
        /// Switch the active position source backend live. Tears down the
        /// current backend's listeners (JNI proxies, Input.location), resets
        /// the state machine, and kicks off init for the new source. Persists
        /// the choice to PlayerPrefs.
        /// </summary>
        public void SwitchPositionSource(PositionSource newSource)
        {
            if (_currentSource == newSource) return;
            Debug.Log($"[GPS] SwitchPositionSource {_currentSource} -> {newSource}");

            // Tear down whatever's running.
            ShutdownCurrentBackend();

            _currentSource = newSource;
            PlayerPrefs.SetString(PrefsSourceKey, newSource.ToString());
            PlayerPrefs.Save();

            _GPSState = GPSState.Uninitialized;
            _gpsRetryCount = 0;
            _lastGpsError = "";
            _gpsInitStartTime = -1f;
            // Don't clear _currentPosition - the user has presumably been
            // playing somewhere. Map markers/avatar position can stay until
            // the next fix arrives from the new source.

            EnsureGPSStarted();
        }

        /// <summary>Cycle through the available sources for tap-to-cycle UI.</summary>
        public void CycleNextPositionSource()
        {
            var values = (PositionSource[])Enum.GetValues(typeof(PositionSource));
            int idx = Array.IndexOf(values, _currentSource);
            var next = values[(idx + 1) % values.Length];
            SwitchPositionSource(next);
        }

        private void ShutdownCurrentBackend()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            if (_androidProvider != null)
            {
                _androidProvider.Shutdown();
                _androidProvider = null;
            }
#endif
            // Stop Unity Input.location too, in case it was running.
            try { Input.location.Stop(); } catch { }
        }

        /// <summary>
        /// Kick off GPS initialization if it hasn't started yet. Safe to call
        /// repeatedly. Hosts must call this from the lobby setup screen so
        /// that by the time they click "Create Lobby" we have a real GPS
        /// fix to use as the play-area center, instead of falling back to
        /// the hardcoded coordinates.
        /// </summary>
        public void EnsureGPSStarted()
        {
            if (_currentSource == PositionSource.EditorWasd) return;
            if (_coroutineHost == null) return;
            // Allow tapping "Create Lobby" again (or any caller of this
            // method) to retry from Failed up to _maxGpsRetries times.
            if (_GPSState == GPSState.Uninitialized)
            {
                _coroutineHost.StartCoroutine(InitiallizeGPS());
            }
            else if (_GPSState == GPSState.Failed && _gpsRetryCount < _maxGpsRetries)
            {
                _gpsRetryCount++;
                _coroutineHost.StartCoroutine(InitiallizeGPS());
            }
        }
        public void positionCheck()
        {
            var state = _gameClient?.CurrentLobbyState;
            if (state == null || state.Phase != GamePhase.Playing)
                return;

            try
            {
                if (_currentSource == PositionSource.EditorWasd)
                {
                    if (_currentPosition == new Position(0, 0))
                    {
                        if (state.MapData == null)
                            return;

                        //Init blok
                        _currentPosition = state.MapData.Center;
                        _mapCenter = state.MapData.Center;
                        _lastSentPosition = _currentPosition;
                    }

                    if (!SuppressWasd)
                        TestPlayerPosition();
                    else
                        TrySendCurrentPosition(); // keep-alive only
                }
                else
                {
                    if (_GPSState == GPSState.Uninitialized)
                    {
                        _coroutineHost.StartCoroutine(InitiallizeGPS());
                        return;
                    }
                    else if (_GPSState == GPSState.Initializing)
                    {
                        return;
                    }
                    else if (_GPSState == GPSState.Running)
                    {
                        EnsureMapCenter();
                        TrySendCurrentPosition();
                    }
                    else
                    {
                        Debug.Log("GPS failed, trying again...");
                        if (_gpsRetryCount < _maxGpsRetries)
                        {
                            _gpsRetryCount++;
                            _GPSState = GPSState.Uninitialized;
                        }
                        else
                        {
                            Debug.LogWarning("GPS unavailable after max retries. Using last known position.");
                            // Keep _GPSState = Failed so we stop retrying
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}");
            }
        }

        private void EnsureMapCenter()
        {
            if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0)
                return;

            var md = _gameClient?.CurrentLobbyState?.MapData;
            if (md != null)
                _mapCenter = md.Center;
        }

        private void TrySendCurrentPosition()
        {
            bool moved = _currentPosition != _lastSentPosition;
            bool keepAliveDue = (Time.time - _lastPositionSendTime) >= _positionKeepAliveSeconds;
            if (!moved && !keepAliveDue)
                return;

            var previous = _lastSentPosition;
            _gameClient.UpdatePosition(_currentPosition);
            _lastSentPosition = _currentPosition;
            _lastPositionSendTime = Time.time;

            if (_player == null || (_mapCenter.Lat == 0 && _mapCenter.Lon == 0))
                return;

            var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
            _player.transform.position = localCurrent;

            if (previous.Lat == 0 && previous.Lon == 0)
                return;

            var heading = CalculateHeading(previous.ToLocalVector3(_mapCenter), localCurrent);
            if (heading.HasValue)
                _player.transform.rotation = Quaternion.Euler(0, (float)heading.Value, 0);
        }

        private void TestPlayerPosition()
        {
            double x = Input.GetAxis("Horizontal");
            double y = Input.GetAxis("Vertical");
            _currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
            var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
            var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
            if (heading != null)
            {
                if (_player != null)
                    _player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
            }
            if (_player != null)
                _player.transform.position = localCurrent;
            try
            {
                TrySendCurrentPosition();
            }
            catch
            {
                _gameClient.UpdatePosition(_currentPosition);
                _lastSentPosition = _currentPosition;
            }
        }
        private double? CalculateHeading(Vector3 first, Vector3 second)
        {
            if ((first - second).magnitude < 0.0001f) return null;
            float dx = second.x - first.x;
            float dz = second.z - first.z;
            float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg;
            if (heading < 0) heading += 360f;
            return heading;
        }
        IEnumerator InitiallizeGPS()
        {
            _GPSState = GPSState.Initializing;
            _gpsInitStartTime = Time.time;
            _lastGpsError = "";

#if UNITY_ANDROID
            // Request fine location permission if not already granted.
            // On Android 12+ a "precise" toggle exists separately from coarse,
            // but Unity's FineLocation request covers both for our purposes.
            if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
            {
                UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.FineLocation);
                // Wait up to 10 seconds for user to respond to the permission dialog
                float waited = 0f;
                while (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation) && waited < 10f)
                {
                    yield return new WaitForSeconds(0.5f);
                    waited += 0.5f;
                }
                if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
                {
                    _lastGpsError = "Permission denied (fine location)";
                    Debug.LogError("[GPS] " + _lastGpsError);
                    _GPSState = GPSState.Failed;
                    yield break;
                }
            }
#endif

#if UNITY_ANDROID && !UNITY_EDITOR
            // Choose subscription scope based on selected source. UnityInput
            // skips JNI entirely and falls through to the Input.location path
            // below (the same path iOS / editor use).
            if (_currentSource == PositionSource.Auto ||
                _currentSource == PositionSource.GpsOnly ||
                _currentSource == PositionSource.NetworkOnly)
            {
                bool useGps     = (_currentSource != PositionSource.NetworkOnly);
                bool useNetwork = (_currentSource != PositionSource.GpsOnly);

                if (_androidProvider != null)
                {
                    _androidProvider.Shutdown();
                    _androidProvider = null;
                }
                _androidProvider = new AndroidLocationProvider();
                if (!_androidProvider.Initialize(out var initError, useGps, useNetwork))
                {
                    _lastGpsError = "Native LocationManager failed: " + initError;
                    Debug.LogError("[GPS] " + _lastGpsError);
                    _androidProvider = null;
                    _GPSState = GPSState.Failed;
                    yield break;
                }

                // Fast-fail if neither subscribed provider is enabled at OS
                // level. Waiting 60s for fixes from disabled providers is
                // pointless - tell the user immediately what's wrong.
                bool anyUsableEnabled =
                    (useGps && _androidProvider.GpsProviderEnabled) ||
                    (useNetwork && _androidProvider.NetworkProviderEnabled);
                if (!anyUsableEnabled)
                {
                    string which = useGps && useNetwork ? "gps + network"
                                 : useGps ? "gps"
                                 : "network";
                    _lastGpsError = $"{which} provider DISABLED at OS level. Open Settings > Location and switch it ON. Or tap [Source] to try a different backend.";
                    Debug.LogError("[GPS] " + _lastGpsError);
                    _androidProvider.Shutdown();
                    _androidProvider = null;
                    _GPSState = GPSState.Failed;
                    yield break;
                }

                // Wait for the first fix (cached or live).
                int maxWaitJni = _gpsInitTimeoutSeconds;
                while (!_androidProvider.HasFix && maxWaitJni > 0)
                {
                    yield return new WaitForSeconds(1);
                    maxWaitJni--;
                }

                if (!_androidProvider.HasFix)
                {
                    string enabled = _androidProvider.EnabledProvidersList ?? "";
                    string gpsState = _androidProvider.GpsProviderEnabled ? "ON" : "OFF";
                    string netState = _androidProvider.NetworkProviderEnabled ? "ON" : "OFF";
                    string lastKnown = $"lastKnown[gps={(_androidProvider.GpsLastKnownExists ? "yes" : "no")}, net={(_androidProvider.NetworkLastKnownExists ? "yes" : "no")}]";

                    _lastGpsError = $"Timeout {_gpsInitTimeoutSeconds}s on {_currentSource}. enabled=[{enabled}] gps={gpsState} net={netState} {lastKnown}. Try [Source] cycle to switch backends.";
                    Debug.LogError("[GPS] " + _lastGpsError);
                    _androidProvider.Shutdown();
                    _androidProvider = null;
                    _GPSState = GPSState.Failed;
                    yield break;
                }

                _currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
                _GPSState = GPSState.Running;
                _gpsRetryCount = 0;
                _coroutineHost.StartCoroutine(AndroidGPSService());
                yield break;
            }

            // _currentSource == UnityInput on Android: fall through to the
            // Input.location path below. This is the recovery path for
            // newer Android phones where JNI's streaming-callbacks don't
            // fire (MIUI/HyperOS background restrictions, approximate-vs-
            // precise permission, minDistance gating on stationary phones).
#endif

            // iOS / editor / non-Android / Android-with-UnityInput-source:
            // use Unity's Input.location.
            if (!Input.location.isEnabledByUser)
            {
                _lastGpsError = "Location services not enabled by user";
                Debug.LogError("[GPS] " + _lastGpsError);
                _GPSState = GPSState.Failed;
                yield break;
            }

            float desiredAccuracyInMeters = 5f;
            float updateDistanceInMeters = 1f;

            Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);

            int maxWait = _gpsInitTimeoutSeconds;
            while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
            {
                yield return new WaitForSeconds(1);
                maxWait--;
            }

            if (maxWait < 1)
            {
                _lastGpsError = $"Timed out after {_gpsInitTimeoutSeconds}s waiting for first fix (try moving outdoors, or tap [Source] to try a different backend)";
                Debug.LogError("[GPS] " + _lastGpsError);
                _GPSState = GPSState.Failed;
                yield break;
            }

            if (Input.location.status == LocationServiceStatus.Failed)
            {
                _lastGpsError = "Unity Input.location reported Failed status";
                Debug.LogError("[GPS] " + _lastGpsError);
                _GPSState = GPSState.Failed;
                yield break;
            }

            _GPSState = GPSState.Running;
            _gpsRetryCount = 0;
            _coroutineHost.StartCoroutine(GPSService());
        }

#if UNITY_ANDROID && !UNITY_EDITOR
        /// <summary>
        /// Mirrors the JNI provider's most recent fix into _currentPosition
        /// every 0.5s so the rest of the game (which polls _currentPosition
        /// indirectly via LastKnownPosition / TrySendCurrentPosition) keeps
        /// working unchanged. Replaces GPSService on Android.
        /// </summary>
        IEnumerator AndroidGPSService()
        {
            while (_GPSState == GPSState.Running && _androidProvider != null)
            {
                if (_androidProvider.HasFix)
                {
                    _currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
                }
                yield return new WaitForSeconds(0.5f);
            }

            // Loop ended (state != Running or provider disposed). Clean up
            // listeners so we don't leak across retries.
            if (_androidProvider != null)
            {
                _androidProvider.Shutdown();
                _androidProvider = null;
            }
        }
#endif
        IEnumerator GPSService()
        {
            while (_GPSState == GPSState.Running)
            {
                if (Input.location.status == LocationServiceStatus.Failed)
                {
                    _lastGpsError = "Location service died after init (provider stopped)";
                    Debug.LogError("[GPS] " + _lastGpsError);
                    _GPSState = GPSState.Failed;
                    yield break;
                }

                // Keep current GPS position fresh; sending is throttled in positionCheck().
                var data = Input.location.lastData;
                _currentPosition = new Position(data.latitude, data.longitude);
                yield return new WaitForSeconds(0.5f);
            }
        }
    }
}
