using GeoSus.Client;
using System.Collections;
using System.Threading.Tasks;
using UnityEngine;
using System.Collections.Generic;
using Subsystems;
using System.Linq;
using UnityEngine.SceneManagement;

namespace Subsystems
{
    public class GameManager_Network
    {
        private const string _serverAddress = "geosus.honzuvkod.dev";
        private const int _serverPort = 7777;
        private GameClient _gameClient;
        private GameManager _manager;
        private bool _pendingMapBuild;

        /// <summary>
        /// Authoritative game state; written here, read by GameManager_UI.
        /// </summary>
        public GameState State { get; } = new GameState();

        public GameManager_Network(GameClient gameClient, GameManager manager)
        {
            _gameClient = gameClient;
            _manager = manager;
            RegisterEventHandlers();
        }

        public async void OpenConnection()
        {
            // Snapshot the lobby we believed we were in BEFORE the new connect
            // attempt. If the client SDK preserved it across a transient drop
            // (P9 fix), this is non-null and we'll send a Reconnect message
            // post-handshake to re-associate with the lobby on the server side.
            // Without it, the next CastVote / TaskComplete / etc. would arrive
            // on a fresh connection the server doesn't recognize and bounce
            // with NOT_IN_LOBBY.
            var rejoinLobbyId = _gameClient.LobbyId;

            int retries = 0;
            int delayMs = 5000;
            while (true)
            {
                Task<bool> state = _gameClient.ConnectAsync(_serverAddress, _serverPort);
                await state;
                if (state.Result)
                {
                    Debug.Log("Connected to server.");

                    // Re-attach to the prior lobby if we had one. Server-side
                    // HandleReconnectAsync will replay missed events and ack
                    // with a ReconnectResponse carrying the snapshot.
                    if (!string.IsNullOrEmpty(rejoinLobbyId))
                    {
                        Debug.Log($"Re-associating with lobby {rejoinLobbyId} after reconnect.");
                        _gameClient.Reconnect(rejoinLobbyId);
                    }
                    break;
                }
                retries++;
                if (retries >= 10)
                {
                    Debug.LogError("Failed to connect after 10 attempts. Giving up.");
                    break;
                }
                Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s...");
                await Task.Delay(delayMs);
                delayMs = Mathf.Min(delayMs * 2, 30000);
            }
        }

        public void RegisterEventHandlers()
        {
            _gameClient.OnConnected    += OnConnected;
            _gameClient.OnDisconnected += OnDisconnected;
            _gameClient.OnError        += OnError;
            _gameClient.OnMessage      += OnMessage;
            _gameClient.OnGameEvent    += OnGameEvent;
        }

        private void OnConnected()
        {
            Debug.Log("Successfully connected to the server.");
            // Tear the reconnect overlay down once the socket is healthy.
            // No-op if it wasn't shown.
            _manager?.uiSubsystem?.HideReconnecting();
        }

        private void OnError(string e) => Debug.LogError($"Network error: {e}");

        private void OnDisconnected(string reason)
        {
            Debug.Log($"Disconnected: {reason}");
            // Show the reconnect overlay only if the user is mid-game; we
            // don't want it flashing during a clean shutdown ("Disposed") or
            // before a real game has started.
            if (reason != "Disposed" && State.Phase != GamePhase.Lobby)
                _manager?.uiSubsystem?.ShowReconnecting();

            if (reason != "Disposed" && _manager != null)
                _manager.StartCoroutine(ReconnectAfterDelay(3f));
        }

        private IEnumerator ReconnectAfterDelay(float seconds)
        {
            yield return new UnityEngine.WaitForSeconds(seconds);
            Debug.Log("Attempting to reconnect...");
            OpenConnection();
        }

        private void OnMessage(Message message)
        {
            switch (message.Type)
            {
                case "CreateLobbyResponse":
                    HandleCreateLobbyResponse(message as CreateLobbyResponse);
                    break;
                case "JoinLobbyResponse":
                    HandleJoinLobbyResponse(message as JoinLobbyResponse);
                    break;
                case "PositionBroadcast":
                    HandlePositionBroadcast(message as PositionBroadcast);
                    break;
                case "Error":
                    HandleErrorMessage(message as ErrorMessage);
                    break;
                case "Ack":
                case "GameEvent":
                    break;
                default:
                    Debug.Log("Received message of type: " + message.Type);
                    break;
            }
        }

        /// <summary>
        /// P9 defensive path: if the server tells us NOT_IN_LOBBY but we still
        /// believe we have a lobby (LobbyId preserved across the transient
        /// disconnect), the lobby association on the server's side of the new
        /// connection is missing - typically a race between OpenConnection's
        /// Reconnect call and an in-flight action message that beat it. Retry
        /// the Reconnect; if the second attempt also bounces, the lobby really
        /// is gone and we'll surface the error to the user.
        /// </summary>
        private void HandleErrorMessage(ErrorMessage err)
        {
            if (err == null) return;
            Debug.Log($"Server error: code={err.ErrorCode} text={err.ErrorText}");

            if (err.ErrorCode == "NOT_IN_LOBBY" && !string.IsNullOrEmpty(_gameClient.LobbyId))
            {
                Debug.Log($"NOT_IN_LOBBY but we still have LobbyId={_gameClient.LobbyId}; resending Reconnect.");
                _gameClient.Reconnect(_gameClient.LobbyId);
            }
        }

        private void OnGameEvent(GameEvent gameEvent)
        {
            // Always sync player list from lobby state after any event
            SyncPlayersFromLobby();

            switch (gameEvent.EventType)
            {
                case "PlayerJoined":
                case "PlayerLeft":
                case "HostChanged":
                    _manager?.uiSubsystem?.NotifyLobbyChanged();
                    break;

                case "GameStarting":
                    State.Phase = GamePhase.Loading;
                    HandleGameStarting();
                    break;

                case "MapDataReady":
                    HandleMapDataReady();
                    break;

                case "GameStarted":
                    State.Phase = GamePhase.Playing;
                    break;

                case "RoleAssigned":
                    HandleRoleAssigned(gameEvent);
                    break;

                case "TaskCompleted":
                    HandleTaskCompleted(gameEvent);
                    break;

                case "PlayerKilled":
                    HandlePlayerKilled(gameEvent);
                    break;

                case "BodyReported":
                case "EmergencyMeetingCalled":
                    Toast("Meeting called! Head to the meeting point.");
                    break;

                case "MeetingStarted":
                    HandleMeetingStarted(gameEvent);
                    break;

                case "PlayerArrivedAtMeeting":
                    HandlePlayerArrivedAtMeeting(gameEvent);
                    break;

                case "PlayerVoted":
                    HandlePlayerVoted(gameEvent);
                    break;

                case "VotingClosed":
                    HandleVotingClosed(gameEvent);
                    break;

                case "GameEnded":
                    HandleGameEnded(gameEvent);
                    break;

                case "ReturnedToLobby":
                    HandleReturnedToLobby();
                    break;

                case "SabotageStarted":
                    HandleSabotageStarted(gameEvent);
                    break;

                case "RepairStarted":
                    HandleRepairStarted(gameEvent);
                    break;

                case "RepairStopped":
                    HandleRepairStopped(gameEvent);
                    break;

                case "SabotageRepaired":
                case "SabotageMeltdown":
                case "SabotageExpired":
                    State.ActiveSabotage = null;
                    State.ActiveRepairs.Clear();
                    _manager?.uiSubsystem?.HideSabotageTimer();
                    _manager?.mapSubsystem?.ClearSabotageMarkers();
                    break;

                case "TaskStarted":
                    // Server now broadcasts when a player begins a task. Phase 1
                    // only acks; Phase 2/3 will surface this to other players.
                    break;

                case "MapDataError":
                    HandleMapDataError(gameEvent);
                    break;

                default:
                    Debug.Log("GameEvent: " + gameEvent.EventType);
                    break;
            }
        }

        // ── Lobby responses ───────────────────────────────────────────────────

        private void HandleCreateLobbyResponse(CreateLobbyResponse message)
        {
            if (message == null) return;
            if (message.Success)
            {
                Debug.Log($"Lobby created. Code: {message.JoinCode}");
                // P13b: snapshot the server's authoritative settings into
                // GameState so HUD / proximity code can read distances and
                // cooldowns from a single source of truth instead of hardcodes.
                State.Settings = _gameClient.CurrentLobbyState?.Settings;
                SceneManager.LoadScene("create", LoadSceneMode.Single);
                _manager?.uiSubsystem?.NotifyLobbyChanged();
            }
            else
            {
                Debug.LogError("Failed to create lobby: " + message.Error);
            }
        }

        private void HandleJoinLobbyResponse(JoinLobbyResponse message)
        {
            if (message == null) return;
            if (message.Success)
            {
                Debug.Log($"Joined lobby: {message.LobbyId}");
                // P13b: same settings snapshot path as host. Joiners read the
                // server's snapshot taken at lobby creation; they cannot edit.
                State.Settings = _gameClient.CurrentLobbyState?.Settings;
                // Unified lobby: both host and joiners land on create.unity.
                // LobbyDisplayUI handles the role split internally (start
                // button for host, waiting text for joiners).
                SceneManager.LoadScene("create", LoadSceneMode.Single);
                _manager?.uiSubsystem?.NotifyLobbyChanged();
            }
            else
            {
                Debug.LogError("Failed to join lobby: " + message.Error);
            }
        }

        // ── Game flow ─────────────────────────────────────────────────────────

        private void HandleGameStarting()
        {
            _pendingMapBuild = false;
            // Reset per-game state
            State.MyRole                = null;
            State.IsDead                = false;
            State.MyTasks               = new List<GameTask>();
            State.MyCompletedTaskIds    = new HashSet<string>();
            State.TotalCompleted        = 0;
            State.TotalRequired         = 0;
            State.ActiveMeeting         = null;
            State.LastVoteResult        = null;
            State.VotedPlayerIds        = new HashSet<string>();
            State.ActiveSabotage        = null;
            State.GameEndData           = null;
            State.KillCooldownRemaining = 0;
            SceneManager.LoadScene("Client", LoadSceneMode.Single);
        }

        private void HandleMapDataReady()
        {
            _pendingMapBuild = true;
            TryBuildMapAndMarkers();
        }

        public void OnClientSceneReady()
        {
            TryBuildMapAndMarkers();
        }

        private void TryBuildMapAndMarkers()
        {
            if (!_pendingMapBuild) return;
            if (_manager?.mapSubsystem == null || !_manager.mapSubsystem.IsSceneReady) return;
            if (_gameClient?.CurrentLobbyState?.MapData == null) return;

            _manager.mapSubsystem.BuildMap();
            _manager.mapSubsystem.CreateTaskMarkers(_gameClient.MyTasks);
            _pendingMapBuild = false;
            Debug.Log("[Network] Map built.");
        }

        private void HandleRoleAssigned(GameEvent evt)
        {
            var payload = evt.GetPayload<RoleAssignedPayload>();
            if (payload == null || payload.ClientUuid != _gameClient.ClientUuid) return;

            State.MyRole  = payload.Role;
            State.MyTasks = payload.Tasks ?? new List<GameTask>();
            State.MyCompletedTaskIds.Clear();

            Debug.Log($"Role: {payload.Role}, Tasks: {State.MyTasks.Count}");
            _manager?.taskSubsystem?.Initialize(State.MyTasks);
        }

        private void HandleTaskCompleted(GameEvent evt)
        {
            var payload = evt.GetPayload<TaskCompletedPayload>();
            if (payload == null) return;

            // Track if it's our task
            if (payload.ClientUuid == _gameClient.ClientUuid)
                State.MyCompletedTaskIds.Add(payload.TaskId);

            State.TotalCompleted = payload.TotalCompleted;
            State.TotalRequired  = payload.TotalTasks;

            _manager?.uiSubsystem?.UpdateTaskProgress(payload.TotalCompleted, payload.TotalTasks);
            _manager?.mapSubsystem?.RemoveTaskMarker(payload.TaskId);
        }

        private void HandlePlayerKilled(GameEvent evt)
        {
            var payload = evt.GetPayload<PlayerKilledPayload>();
            if (payload == null) return;

            _manager?.mapSubsystem?.CreateBodyMarker(payload.BodyId, payload.Location);

            if (payload.VictimId == _gameClient.ClientUuid)
            {
                State.IsDead = true;
                _manager?.uiSubsystem?.OnLocalPlayerDied();
            }

            // Update player state in our list
            var p = State.Players.Find(x => x.ClientUuid == payload.VictimId);
            if (p != null) p.State = PlayerState.Dead;
        }

        private void HandleMeetingStarted(GameEvent evt)
        {
            var payload = evt.GetPayload<MeetingStartedPayload>();
            if (payload == null) return;

            State.Phase             = GamePhase.Meeting;
            State.ActiveMeeting     = payload;
            State.VotedPlayerIds    = new HashSet<string>();
            State.ArrivedPlayerIds  = new HashSet<string>();
            State.VoterTargets      = new Dictionary<string, string>();
            State.VoteTallies       = new Dictionary<string, int>();
            State.MyVoteTarget      = null;
            State.LastVoteResult    = null;

            SyncPlayersFromLobby();
            _manager?.uiSubsystem?.ShowMeetingPanel(State.Players, payload);
        }

        private void HandlePlayerArrivedAtMeeting(GameEvent evt)
        {
            var payload = evt.GetPayload<PlayerArrivedAtMeetingPayload>();
            if (payload == null) return;
            State.ArrivedPlayerIds.Add(payload.ClientUuid);
        }

        private void HandlePlayerVoted(GameEvent evt)
        {
            var payload = evt.GetPayload<PlayerVotedPayload>();
            if (payload == null) return;

            // Server allows vote changes within a 2s rate limit, so we always
            // overwrite the voter's previous target rather than appending.
            string target = payload.TargetId ?? GameState.VoteSkip;

            State.VotedPlayerIds.Add(payload.VoterId);
            State.VoterTargets[payload.VoterId] = target;
            RecomputeVoteTallies();

            if (payload.VoterId == _gameClient.ClientUuid)
                State.MyVoteTarget = target;
        }

        private void RecomputeVoteTallies()
        {
            State.VoteTallies.Clear();
            foreach (var t in State.VoterTargets.Values)
            {
                if (string.IsNullOrEmpty(t)) continue;
                State.VoteTallies.TryGetValue(t, out var count);
                State.VoteTallies[t] = count + 1;
            }
        }

        private void HandleVotingClosed(GameEvent evt)
        {
            var payload = evt.GetPayload<VotingClosedPayload>();
            if (payload == null) return;

            State.Phase          = GamePhase.Playing;
            State.ActiveMeeting  = null;
            State.LastVoteResult = payload;

            // Mark ejected player dead in our list
            if (!string.IsNullOrEmpty(payload.EjectedPlayerId))
            {
                var p = State.Players.Find(x => x.ClientUuid == payload.EjectedPlayerId);
                if (p != null) p.State = PlayerState.Dead;
            }

            _manager?.uiSubsystem?.ShowVoteResult(payload, State.Players);
            _manager?.mapSubsystem?.ClearBodyMarkers();
        }

        private void HandleGameEnded(GameEvent evt)
        {
            var payload = evt.GetPayload<GameEndedPayload>();
            if (payload == null) return;

            State.Phase       = GamePhase.Ended;
            State.GameEndData = payload;

            // If the round ended while the meeting/vote-result overlay was
            // still up (e.g. ejection won the game outright), the auto-close
            // coroutine would otherwise fire 5s later and tear down the
            // meeting panel while the GameEndPanel sits on top - leaving a
            // glimpse of the dead overlay during the transition.
            _manager?.uiSubsystem?.HideMeetingPanel();
            _manager?.uiSubsystem?.ShowGameEndPanel(payload, _gameClient.ClientUuid);
        }

        private void HandleReturnedToLobby()
        {
            State.Phase = GamePhase.Lobby;
            _manager?.uiSubsystem?.HideMeetingPanel();
            // Bodies survive the scene reload because the marker GameObjects are
            // parented under MapCenterPoint (which lives in the persistent
            // Client.unity scene). Without this clear, returning to lobby and
            // starting a new round leaves stale corpses on the map of the new
            // round. Server already cleared its `_bodies` set in
            // ProcessReturnToLobby; this is the client-side mirror that was
            // missing in HandleVotingClosed's symmetry.
            _manager?.mapSubsystem?.ClearBodyMarkers();
            _manager?.mapSubsystem?.ClearSabotageMarkers();
            // Unified lobby: regardless of role, return to create.unity.
            SceneManager.LoadScene("create", LoadSceneMode.Single);
        }

        private void HandleSabotageStarted(GameEvent evt)
        {
            var payload = evt.GetPayload<SabotageStartedPayload>();
            if (payload == null) return;

            State.ActiveSabotage = payload;
            State.ActiveRepairs.Clear();

            _manager?.mapSubsystem?.CreateSabotageMarkers(payload.RepairStations);
            if (payload.Type == SabotageType.CriticalMeltdown && payload.Deadline.HasValue)
                _manager?.uiSubsystem?.ShowSabotageTimer(payload.Deadline.Value);
            if (payload.Type == SabotageType.CommsBlackout)
                _manager?.uiSubsystem?.SetCommsBlackout(true);
        }

        private void HandleRepairStarted(GameEvent evt)
        {
            var payload = evt.GetPayload<RepairStartedPayload>();
            if (payload == null || string.IsNullOrEmpty(payload.StationId)) return;
            State.ActiveRepairs.Add(payload.StationId);
        }

        private void HandleRepairStopped(GameEvent evt)
        {
            // A player abandoned a repair station mid-fix. The station is no
            // longer counted as active for the simultaneous-repair coaching;
            // the marker stays on the map until the sabotage resolves.
            var payload = evt.GetPayload<RepairStoppedPayload>();
            if (payload != null && !string.IsNullOrEmpty(payload.StationId))
                State.ActiveRepairs.Remove(payload.StationId);
        }

        private void HandleMapDataError(GameEvent evt)
        {
            // Server failed to fetch Overpass data. Without this the loading
            // screen would hang forever. Drop back to lobby and surface the
            // failure so the player can re-host or try a different center.
            Debug.LogError("[Network] Server could not generate map data.");
            State.Phase = GamePhase.Lobby;
            _manager?.uiSubsystem?.ShowToast("Map fetch failed. Returning to lobby.");
            LeaveLobby();
        }

        private void HandlePositionBroadcast(PositionBroadcast broadcast)
        {
            if (broadcast == null) return;
            _manager?.mapSubsystem?.UpdatePlayerAvatars(_gameClient.PlayerPositions, _gameClient.ClientUuid);
        }

        // ── Helpers ───────────────────────────────────────────────────────────

        private void SyncPlayersFromLobby()
        {
            var lobby = _gameClient.CurrentLobbyState;
            if (lobby?.Players != null)
                State.Players = lobby.Players;
        }

        private void Toast(string message)
        {
            State.ToastMessage = message;
            State.ToastExpiry  = UnityEngine.Time.time + 4f;
        }

        // ── Send helpers ──────────────────────────────────────────────────────

        public void CreateLobby(double lat, double lon, double radius = 500, int impostorCount = 1, int taskCount = 5, GameSettingsOverrides settings = null)
        {
            _gameClient.CreateLobby(new Position(lat, lon), impostorCount, taskCount, null, radius, settings);
        }

        public void JoinLobby(string joinCode)
        {
            try { _gameClient.JoinLobby(joinCode); }
            catch (System.Exception ex) { Debug.LogError("JoinLobby error: " + ex.Message); }
        }

        public void LeaveLobby()
        {
            _gameClient.LeaveLobby();
            State.Phase = GamePhase.Lobby;
            SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single);
        }

        public void StartGame()
        {
            _gameClient.StartGame();
        }
    }
}

