namespace GeoSus.Server;

using Microsoft.Extensions.Logging;
using System.Threading.Channels;

// Per-lobby actor - single-threaded zpracování všech mutací
public class LobbyActor : IDisposable
{
    private readonly string _lobbyId;
    private readonly ServerConfig _config;
    private readonly ILogger<LobbyActor> _logger;
    private readonly StatsDb _statsDb;
    private readonly Persistence _persistence;
    private readonly GameLogic _gameLogic;
    private readonly AntiCheat _antiCheat;
    private readonly OverpassService? _overpassService;
    
    // Actor channel - deterministic single-threaded processing
    private readonly Channel<LobbyAction> _actionChannel;
    private readonly CancellationTokenSource _cts = new();
    private readonly Task _processingTask;
    
    // State
    private readonly Dictionary<string, Player> _players = new();
    private readonly Dictionary<string, ClientConnection> _connections = new();
    private readonly List<Body> _bodies = new();
    private readonly List<GameTask> _tasks = new();
    private Meeting? _currentMeeting;
    private Sabotage? _currentSabotage;
    private DateTime? _lastSabotageTime;
    private GamePhase _phase = GamePhase.Lobby;
    private long _eventId;
    private long _serverSeq;
    private DateTime _lastActivity = DateTime.UtcNow;
    private int _snapshotEventCounter;
    private DateTime _lastSnapshotTime = DateTime.UtcNow;
    
    // Map data loading tracking
    private readonly HashSet<string> _playersWithMapData = new();
    
    public LobbySettings Settings { get; }
    public GamePhase Phase => _phase;
    public int PlayerCount => _players.Count;
    public DateTime LastActivity => _lastActivity;
    public DateTime CreatedAt { get; } = DateTime.UtcNow;
    
    // Admin panel properties
    public string LobbyId => _lobbyId;
    public string JoinCode => Settings.JoinCode;
    public DateTime? PhaseEndTime { get; private set; }
    public Sabotage? ActiveSabotage => _currentSabotage;
    public MapData? MapData => Settings.MapData;
    
    public LobbyActor(
        string lobbyId, 
        LobbySettings settings, 
        ServerConfig config, 
        ILogger<LobbyActor> logger,
        StatsDb statsDb,
        Persistence persistence,
        OverpassService? overpassService = null)
    {
        _lobbyId = lobbyId;
        Settings = settings;
        _config = config;
        _logger = logger;
        _statsDb = statsDb;
        _persistence = persistence;
        _overpassService = overpassService;
        _gameLogic = new GameLogic(config, logger, overpassService);
        _antiCheat = new AntiCheat(config, logger);
        
        // Emergency meeting location = center of play area
        Settings.EmergencyMeetingLocation = Settings.PlayAreaCenter;
        
        _actionChannel = Channel.CreateUnbounded<LobbyAction>(new UnboundedChannelOptions
        {
            SingleReader = true,
            SingleWriter = false
        });
        
        _processingTask = Task.Run(ProcessActionsAsync);
        
        _logger.LogInformation("LobbyActor vytvořen: {LobbyId}", lobbyId);
    }
    
    #region Public API - thread-safe enqueue
    
    public async Task AddPlayerAsync(string clientUuid, string displayName, bool isOwner)
    {
        var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        await EnqueueAsync(new AddPlayerAction 
        { 
            ClientUuid = clientUuid, 
            DisplayName = displayName, 
            IsOwner = isOwner,
            Completion = completion
        });
        // Čekáme na dokončení zpracování akce
        await completion.Task;
    }
    
    public void RegisterConnection(string clientUuid, ClientConnection connection)
    {
        _connections[clientUuid] = connection;
    }
    
    public void UnregisterConnection(string clientUuid)
    {
        _connections.TryRemove(clientUuid, out _);
    }
    
    public async Task HandleMessageAsync(string clientUuid, Message message)
    {
        _lastActivity = DateTime.UtcNow;
        
        await EnqueueAsync(new HandleMessageAction 
        { 
            ClientUuid = clientUuid, 
            Message = message 
        });
    }
    
    public async Task RemovePlayerAsync(string clientUuid, string? reason = null)
    {
        await EnqueueAsync(new RemovePlayerAction 
        { 
            ClientUuid = clientUuid, 
            Reason = reason 
        });
    }
    
    public LobbyState GetLobbyState()
    {
        return new LobbyState
        {
            LobbyId = _lobbyId,
            JoinCode = Settings.JoinCode,
            OwnerId = _players.Values.FirstOrDefault(p => p.IsOwner)?.ClientUuid,
            Phase = _phase,
            Players = _players.Values.Select(p => new PlayerInfo
            {
                ClientUuid = p.ClientUuid,
                DisplayName = p.DisplayName,
                IsOwner = p.IsOwner,
                IsReady = p.IsReady,
                State = p.State
            }).ToList(),
            PlayAreaCenter = Settings.PlayAreaCenter,
            PlayAreaRadius = Settings.PlayAreaRadius,
            ImpostorCount = Settings.ImpostorCount,
            HasPassword = !string.IsNullOrEmpty(Settings.Password),
            CreatedAt = _lastActivity,
            MapData = Settings.MapDataPayload,
            MapDataReady = Settings.MapDataPayload != null || !Settings.OverpassEnabled
        };
    }
    
    /// <summary>
    /// Get all players for admin panel spectate view
    /// </summary>
    public IEnumerable<AdminPlayerInfo> GetPlayers()
    {
        return _players.Values.Select(p => new AdminPlayerInfo
        {
            PlayerId = p.ClientUuid,
            DisplayName = p.DisplayName,
            State = p.State,
            Role = p.Role,
            Position = p.Position,
            IsHost = p.IsOwner,
            CheatScore = p.CheatScore,
            KillCooldownEnd = p.LastKillTime?.AddMilliseconds(_config.KillCooldownMs),
            VotedFor = _currentMeeting?.Votes.TryGetValue(p.ClientUuid, out var vote) == true ? vote : null
        });
    }
    
    /// <summary>
    /// Get all tasks for admin panel spectate view
    /// </summary>
    public IEnumerable<AdminTaskInfo> GetTasks()
    {
        // Build completed by sets from player data
        var completedByTask = new Dictionary<string, HashSet<string>>();
        foreach (var player in _players.Values)
        {
            foreach (var taskId in player.CompletedTaskIds)
            {
                if (!completedByTask.ContainsKey(taskId))
                    completedByTask[taskId] = new HashSet<string>();
                completedByTask[taskId].Add(player.ClientUuid);
            }
        }
        
        return _tasks.Select(t => new AdminTaskInfo
        {
            TaskId = t.TaskId,
            Name = t.Name,
            Location = t.Location,
            CompletedBy = completedByTask.TryGetValue(t.TaskId, out var set) ? set : new HashSet<string>()
        });
    }
    
    /// <summary>
    /// Get all bodies for admin panel spectate view
    /// </summary>
    public IEnumerable<AdminBodyInfo> GetBodies()
    {
        return _bodies.Select(b => new AdminBodyInfo
        {
            VictimId = b.VictimId,
            Position = b.Location,
            ReportedAt = b.Reported ? DateTime.UtcNow : null
        });
    }
    
    /// <summary>
    /// Get current votes during voting phase
    /// </summary>
    public Dictionary<string, string?>? GetVotes()
    {
        return _currentMeeting?.Votes;
    }
    
    /// <summary>
    /// Broadcast system message to all players in lobby
    /// </summary>
    public void BroadcastSystemMessage(string message)
    {
        var evt = CreateEvent("SystemMessage", "server", new SystemMessagePayload
        {
            Message = message,
            Timestamp = DateTime.UtcNow
        });
        
        PersistAndBroadcast(evt);
        _logger.LogInformation("System message broadcast to lobby {LobbyId}: {Message}", _lobbyId, message);
    }
    
    /// <summary>
    /// Kick player from lobby (admin action)
    /// </summary>
    public bool TryKickPlayer(string playerId, string reason)
    {
        if (!_players.ContainsKey(playerId))
            return false;
            
        // Odstraníme hráče asynchronně
        _ = RemovePlayerAsync(playerId, reason);
        return true;
    }

    public async Task ArchiveAndCloseAsync()
    {
        _cts.Cancel();
        await _processingTask;
        
        // Uložíme finální snapshot
        SaveSnapshot();
        
        // Archivujeme
        _persistence.ArchiveLobby(_lobbyId);
    }
    
    #endregion
    
    #region Actor processing
    
    private async Task EnqueueAsync(LobbyAction action)
    {
        await _actionChannel.Writer.WriteAsync(action, _cts.Token);
    }
    
    private async Task ProcessActionsAsync()
    {
        try
        {
            await foreach (var action in _actionChannel.Reader.ReadAllAsync(_cts.Token))
            {
                try
                {
                    await ProcessActionAsync(action);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Chyba při zpracování akce {Action}", action.GetType().Name);
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("LobbyActor {LobbyId} ukončen", _lobbyId);
        }
    }
    
    private async Task ProcessActionAsync(LobbyAction action)
    {
        try
        {
            switch (action)
            {
                case AddPlayerAction a:
                    ProcessAddPlayer(a);
                    break;
                case RemovePlayerAction a:
                    ProcessRemovePlayer(a);
                    break;
                case HandleMessageAction a:
                    await ProcessMessageAsync(a);
                    break;
                case CloseMeetingAction a:
                    if (_currentMeeting?.MeetingId == a.MeetingId)
                    {
                        CloseMeeting();
                    }
                    break;
                case CheckMeetingArrivalAction a:
                    if (_currentMeeting?.MeetingId == a.MeetingId)
                    {
                        CheckAllPlayersArrival();
                    }
                    break;
                case RepairCompleteAction a:
                    ProcessRepairComplete(a);
                    break;
                case SabotageMeltdownAction a:
                    ProcessSabotageMeltdown(a);
                    break;
                case SabotageExpireAction a:
                    ProcessSabotageExpire(a);
                    break;
            }
            
            // Signalizace dokončení pro akce, které na to čekají
            action.Completion?.TrySetResult();
        }
        catch (Exception ex)
        {
            action.Completion?.TrySetException(ex);
            throw;
        }
        
        // Periodické snapshoty
        CheckSnapshot();
    }
    
    #endregion
    
    #region Player management
    
    private void ProcessAddPlayer(AddPlayerAction action)
    {
        if (_players.ContainsKey(action.ClientUuid))
        {
            // Reconnect
            _players[action.ClientUuid].ConnectedAt = DateTime.UtcNow;
            _logger.LogInformation("Hráč {Uuid} reconnected", action.ClientUuid);
            return;
        }
        
        var player = new Player
        {
            ClientUuid = action.ClientUuid,
            DisplayName = action.DisplayName,
            IsOwner = action.IsOwner,
            Position = Settings.PlayAreaCenter
        };
        
        _players[action.ClientUuid] = player;
        _statsDb.EnsurePlayerExists(action.ClientUuid, action.DisplayName);
        
        var evt = CreateEvent("PlayerJoined", action.ClientUuid, new PlayerJoinedPayload
        {
            ClientUuid = action.ClientUuid,
            DisplayName = action.DisplayName
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Hráč {Uuid} ({Name}) přidán do lobby", action.ClientUuid, action.DisplayName);
    }
    
    private void ProcessRemovePlayer(RemovePlayerAction action)
    {
        if (!_players.TryGetValue(action.ClientUuid, out var player))
            return;
            
        var wasOwner = player.IsOwner;
        _players.Remove(action.ClientUuid);
        _connections.TryRemove(action.ClientUuid, out _);
        
        var evt = CreateEvent("PlayerLeft", action.ClientUuid, new PlayerLeftPayload
        {
            ClientUuid = action.ClientUuid,
            Reason = action.Reason
        });
        
        PersistAndBroadcast(evt);
        
        // Host migration pokud odešel owner
        if (wasOwner && _players.Count > 0)
        {
            ElectNewOwner();
        }
        
        _logger.LogInformation("Hráč {Uuid} opustil lobby: {Reason}", action.ClientUuid, action.Reason);
    }
    
    private void ElectNewOwner()
    {
        // Nejstarší hráč podle připojení
        var newOwner = _players.Values
            .OrderBy(p => p.ConnectedAt)
            .ThenBy(p => p.ClientUuid)
            .FirstOrDefault();
            
        if (newOwner == null) return;
        
        var previousOwnerId = _players.Values.FirstOrDefault(p => p.IsOwner)?.ClientUuid ?? "none";
        
        foreach (var p in _players.Values)
            p.IsOwner = false;
            
        newOwner.IsOwner = true;
        
        var evt = CreateEvent("HostChanged", newOwner.ClientUuid, new HostChangedPayload
        {
            NewHostId = newOwner.ClientUuid,
            PreviousHostId = previousOwnerId
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Nový owner: {Uuid}", newOwner.ClientUuid);
    }
    
    #endregion
    
    #region Message processing
    
    private async Task ProcessMessageAsync(HandleMessageAction action)
    {
        var clientUuid = action.ClientUuid;
        var message = action.Message;
        
        if (!_players.TryGetValue(clientUuid, out var player))
        {
            _logger.LogWarning("Zpráva od neznámého hráče: {Uuid}", clientUuid);
            return;
        }
        
        // Ack pro všechny zprávy
        var ack = new Ack { AckedSeq = message.ClientSeq, Success = true };
        
        switch (message)
        {
            case UpdatePosition m:
                ProcessUpdatePosition(player, m);
                break;
                
            case StartGame m:
                // StartGame is async - it handles its own ack
                await ProcessStartGame(player, m, ack);
                return;
                
            case KillAttempt m:
                ProcessKillAttempt(player, m, ref ack);
                break;
                
            case ReportBody m:
                ProcessReportBody(player, m, ref ack);
                break;
                
            case CallEmergencyMeeting m:
                ProcessEmergencyMeeting(player, m, ref ack);
                break;
                
            case CastVote m:
                ProcessCastVote(player, m, ref ack);
                break;
                
            case TaskComplete m:
                ProcessTaskComplete(player, m, ref ack);
                break;
                
            case Ping m:
                var pong = new Pong 
                { 
                    ClientTime = m.ClientTime, 
                    ServerTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() 
                };
                SendTo(clientUuid, pong);
                return; // Ping nepotřebuje ack
                
            case LeaveLobby:
                await RemovePlayerAsync(clientUuid, "Hráč opustil");
                return;
                
            case KickPlayer m:
                ProcessKickPlayer(player, m, ref ack);
                break;
                
            case StartSabotage m:
                ProcessStartSabotage(player, m, ref ack);
                break;
                
            case ActivateRepairStation m:
                ProcessActivateRepairStation(player, m, ref ack);
                break;
                
            case DeactivateRepairStation m:
                ProcessDeactivateRepairStation(player, m, ref ack);
                break;
                
            case MapDataReceived m:
                ProcessMapDataReceived(player, m, ref ack);
                break;
                
            case ReturnToLobby:
                ProcessReturnToLobby(player, ref ack);
                break;
        }
        
        // Poslat ack
        SendTo(clientUuid, ack);
    }
    
    #endregion
    
    #region Game actions
    
    private void ProcessUpdatePosition(Player player, UpdatePosition message)
    {
        var violation = _antiCheat.ValidateMovement(player, message.Position);
        
        if (violation != null && player.CheatStatus == CheatStatus.Kicked)
        {
            // Kicknout hráče
            _statsDb.IncrementCheatIncidents(player.ClientUuid);
            _ = RemovePlayerAsync(player.ClientUuid, "Anti-cheat kick");
            return;
        }
        
        player.Position = message.Position;
        player.LastPositionUpdate = DateTime.UtcNow;
        
        // Kontrola meeting arrival
        if (_currentMeeting != null && !_currentMeeting.ArrivedPlayers.Contains(player.ClientUuid))
        {
            if (_gameLogic.CheckPlayerArrival(player, _currentMeeting))
            {
                // Hráč právě dorazil na meeting - pošleme event
                var arrivalEvt = CreateEvent("PlayerArrivedAtMeeting", player.ClientUuid, new PlayerArrivedAtMeetingPayload
                {
                    ClientUuid = player.ClientUuid,
                    MeetingId = _currentMeeting.MeetingId
                });
                PersistAndBroadcast(arrivalEvt);
            }
        }
    }
    
    private async Task ProcessStartGame(Player player, StartGame message, Ack ack)
    {
        if (!player.IsOwner)
        {
            ack.Success = false;
            ack.Error = "Pouze owner může spustit hru";
            SendTo(player.ClientUuid, ack);
            return;
        }
        
        if (_phase != GamePhase.Lobby)
        {
            ack.Success = false;
            ack.Error = "Hra již probíhá";
            SendTo(player.ClientUuid, ack);
            return;
        }
        
        if (_players.Count < 2)
        {
            ack.Success = false;
            ack.Error = "Minimum 2 hráči";
            SendTo(player.ClientUuid, ack);
            return;
        }
        
        // Enter loading phase - notify clients to show loading screen
        _phase = GamePhase.Loading;
        _playersWithMapData.Clear();
        
        var loadingEvt = CreateEvent("GameStarting", player.ClientUuid, new GameStartingPayload
        {
            Message = "Načítám mapová data..."
        });
        PersistAndBroadcast(loadingEvt);
        
        // Send ack immediately
        ack.Success = true;
        SendTo(player.ClientUuid, ack);
        
        _logger.LogInformation("Lobby {LobbyId} entering loading phase, fetching map data", _lobbyId);
        
        // Fetch map data from Overpass API
        try
        {
            if (_overpassService != null)
            {
                var mapData = await _overpassService.FetchMapDataAsync(
                    Settings.PlayAreaCenter,
                    Settings.PlayAreaRadius);
                
                Settings.MapData = mapData;
                
                _logger.LogInformation("Lobby {LobbyId} map data loaded: {Buildings} buildings, {Pathways} pathways, {POIs} POIs",
                    _lobbyId, 
                    mapData?.Buildings?.Count ?? 0, 
                    mapData?.Pathways?.Count ?? 0,
                    mapData?.PointsOfInterest?.Count ?? 0);
            }
            
            // Broadcast map data to all clients
            var mapDataPayload = Settings.MapData != null 
                ? MapDataPayload.FromMapData(Settings.MapData) 
                : null;
            var mapDataEvt = CreateEvent("MapDataReady", player.ClientUuid, new MapDataReadyPayload
            {
                MapData = mapDataPayload,
                PlayAreaCenter = Settings.PlayAreaCenter,
                PlayAreaRadius = Settings.PlayAreaRadius
            });
            PersistAndBroadcast(mapDataEvt);
            
            _logger.LogInformation("Lobby {LobbyId} map data distributed to {Count} players, waiting for confirmations",
                _lobbyId, _players.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fetch map data for lobby {LobbyId}", _lobbyId);
            
            // Revert to lobby phase on error
            _phase = GamePhase.Lobby;
            
            var errorEvt = CreateEvent("MapDataError", player.ClientUuid, new { Error = "Nepodařilo se načíst mapová data" });
            PersistAndBroadcast(errorEvt);
        }
    }
    
    private void ProcessMapDataReceived(Player player, MapDataReceived message, ref Ack ack)
    {
        if (_phase != GamePhase.Loading)
        {
            ack.Success = false;
            ack.Error = "Hra není ve fázi načítání";
            return;
        }
        
        _playersWithMapData.Add(player.ClientUuid);
        
        _logger.LogDebug("Player {Player} confirmed map data receipt ({Count}/{Total})",
            player.DisplayName, _playersWithMapData.Count, _players.Count);
        
        // Broadcast progress to all clients
        var progressEvt = CreateEvent("PlayerMapDataReceived", player.ClientUuid, new PlayerMapDataReceivedPayload
        {
            ClientUuid = player.ClientUuid,
            DisplayName = player.DisplayName,
            PlayersReady = _playersWithMapData.Count,
            TotalPlayers = _players.Count
        });
        PersistAndBroadcast(progressEvt);
        
        // Check if all players have received map data
        if (_playersWithMapData.Count >= _players.Count)
        {
            _logger.LogInformation("All players confirmed map data receipt, starting game in lobby {LobbyId}", _lobbyId);
            StartGameAfterMapDataLoaded();
        }
    }
    
    private void StartGameAfterMapDataLoaded()
    {
        // Now actually start the game
        _phase = GamePhase.Playing;
        
        // Assign roles first
        var playerList = _players.Values.ToList();
        _gameLogic.AssignRoles(playerList, Settings.ImpostorCount);
        
        // Generate unique tasks for each crew member
        _tasks.Clear();
        int taskIndex = 0;
        foreach (var p in _players.Values)
        {
            if (p.Role == PlayerRole.Crew)
            {
                // Generate unique tasks for this player
                var playerTasks = _gameLogic.GenerateTasks(
                    Settings.TaskCount, 
                    Settings.PlayAreaCenter, 
                    Settings.PlayAreaRadius,
                    Settings.MapData,
                    taskIndex); // Pass index for unique task IDs
                
                p.Tasks = playerTasks;
                _tasks.AddRange(playerTasks);
                taskIndex += Settings.TaskCount;
            }
        }
        
        // Find who triggered the start (owner)
        var owner = _players.Values.FirstOrDefault(p => p.IsOwner);
        var ownerUuid = owner?.ClientUuid ?? "server";
        
        // Broadcast GameStarted
        var startEvt = CreateEvent("GameStarted", ownerUuid, new GameStartedPayload
        {
            ImpostorCount = Settings.ImpostorCount,
            TaskCount = Settings.TaskCount
        });
        PersistAndBroadcast(startEvt);
        
        // Send each player their role and their own tasks
        foreach (var p in _players.Values)
        {
            var roleEvt = CreateEvent("RoleAssigned", p.ClientUuid, new RoleAssignedPayload
            {
                ClientUuid = p.ClientUuid,
                Role = p.Role,
                Tasks = p.Role == PlayerRole.Crew ? p.Tasks : null
            });
            
            // Role is sent only to that player
            PersistEvent(roleEvt);
            SendTo(p.ClientUuid, roleEvt);
        }
        
        _logger.LogInformation("Game started in lobby {LobbyId}", _lobbyId);
    }
    
    private void ProcessKillAttempt(Player killer, KillAttempt message, ref Ack ack)
    {
        if (_phase != GamePhase.Playing)
        {
            ack.Success = false;
            ack.Error = "Hra neprobíhá";
            return;
        }
        
        if (_currentMeeting != null)
        {
            ack.Success = false;
            ack.Error = "Probíhá meeting";
            return;
        }
        
        if (!_antiCheat.CanPerformAction(killer, "Kill"))
        {
            ack.Success = false;
            ack.Error = "Akce blokována";
            return;
        }
        
        if (!_players.TryGetValue(message.TargetClientUuid, out var victim))
        {
            ack.Success = false;
            ack.Error = "Oběť neexistuje";
            return;
        }
        
        var (success, error, body) = _gameLogic.TryKill(killer, victim, _players, _bodies);
        
        if (!success)
        {
            ack.Success = false;
            ack.Error = error;
            return;
        }
        
        // Update stats
        _statsDb.IncrementKills(killer.ClientUuid);
        _statsDb.IncrementDeaths(victim.ClientUuid);
        
        var evt = CreateEvent("PlayerKilled", killer.ClientUuid, new PlayerKilledPayload
        {
            VictimId = victim.ClientUuid,
            KillerId = killer.ClientUuid,
            BodyId = body!.BodyId,
            Location = body.Location
        });
        
        PersistAndBroadcast(evt);
        
        // Check win conditions
        CheckWinConditions();
    }
    
    private void ProcessReportBody(Player reporter, ReportBody message, ref Ack ack)
    {
        if (_phase != GamePhase.Playing)
        {
            ack.Success = false;
            ack.Error = "Hra neprobíhá";
            return;
        }
        
        if (_currentMeeting != null)
        {
            ack.Success = false;
            ack.Error = "Probíhá meeting";
            return;
        }
        
        // Check comms sabotage
        if (IsCommsBlocked())
        {
            ack.Success = false;
            ack.Error = "Komunikace je rušena - nelze reportovat";
            return;
        }
        
        // Check reactor meltdown - must repair first!
        if (IsMeltdownActive())
        {
            ack.Success = false;
            ack.Error = "KRITICKÁ HAVARIE! Nelze reportovat - nejprve opravte reaktor!";
            return;
        }
        
        var (success, error, body) = _gameLogic.TryReportBody(reporter, message.BodyId, _bodies);
        
        if (!success)
        {
            ack.Success = false;
            ack.Error = error;
            return;
        }
        
        _statsDb.IncrementBodiesReported(reporter.ClientUuid);
        
        var reportEvt = CreateEvent("BodyReported", reporter.ClientUuid, new BodyReportedPayload
        {
            ReporterId = reporter.ClientUuid,
            BodyId = body!.BodyId,
            VictimId = body.VictimId
        });
        PersistAndBroadcast(reportEvt);
        
        // Cancel any active sabotage when meeting starts
        if (_currentSabotage != null && _currentSabotage.State == SabotageState.Active)
        {
            _currentSabotage.State = SabotageState.Repaired;
            _currentSabotage = null;
        }
        
        // Start meeting at body location
        StartMeeting(MeetingType.BodyReport, reporter.ClientUuid, body.Location, body.BodyId);
    }
    
    private void ProcessEmergencyMeeting(Player caller, CallEmergencyMeeting message, ref Ack ack)
    {
        if (_phase != GamePhase.Playing)
        {
            ack.Success = false;
            ack.Error = "Hra neprobíhá";
            return;
        }
        
        if (_currentMeeting != null)
        {
            ack.Success = false;
            ack.Error = "Probíhá meeting";
            return;
        }
        
        // Check comms sabotage
        if (IsCommsBlocked())
        {
            ack.Success = false;
            ack.Error = "Komunikace je rušena - nelze svolat meeting";
            return;
        }
        
        // Check reactor meltdown - must repair first!
        if (IsMeltdownActive())
        {
            ack.Success = false;
            ack.Error = "KRITICKÁ HAVARIE! Nelze svolat meeting - nejprve opravte reaktor!";
            return;
        }
        
        // Kontrola vzdálenosti od středu mapy (emergency button)
        var distanceToCenter = caller.Position.DistanceTo(Settings.EmergencyMeetingLocation);
        if (distanceToCenter > _config.EmergencyMeetingCallRadiusM)
        {
            ack.Success = false;
            ack.Error = $"Musíš být u emergency buttonu (střed mapy) - vzdálenost: {distanceToCenter:F0}m";
            return;
        }
        
        var (success, error) = _gameLogic.TryCallEmergencyMeeting(
            caller, 
            Settings.MaxEmergencyMeetingsPerPlayer, 
            Settings.EmergencyMeetingCooldownMs);
        
        if (!success)
        {
            ack.Success = false;
            ack.Error = error;
            return;
        }
        
        _statsDb.IncrementEmergencyMeetings(caller.ClientUuid);
        
        var callEvt = CreateEvent("EmergencyMeetingCalled", caller.ClientUuid, new EmergencyMeetingCalledPayload
        {
            CallerId = caller.ClientUuid
        });
        PersistAndBroadcast(callEvt);
        
        // Cancel any active sabotage when meeting starts
        if (_currentSabotage != null && _currentSabotage.State == SabotageState.Active)
        {
            _currentSabotage.State = SabotageState.Repaired;
            _currentSabotage = null;
        }
        
        // Start meeting at emergency location
        StartMeeting(MeetingType.Emergency, caller.ClientUuid, Settings.EmergencyMeetingLocation, null);
    }
    
    private void StartMeeting(MeetingType type, string callerId, Position location, string? bodyId)
    {
        _phase = GamePhase.Meeting;
        
        _currentMeeting = _gameLogic.StartMeeting(
            type, callerId, location, bodyId,
            _config.ArrivalBaseMs, _config.VotingPhaseMs, _config.DiscussionPhaseMs);
        
        var evt = CreateEvent("MeetingStarted", callerId, new MeetingStartedPayload
        {
            MeetingId = _currentMeeting.MeetingId,
            Type = type,
            MeetingLocation = location,
            ArrivalDeadline = _currentMeeting.ArrivalDeadline,
            DiscussionEndTime = _currentMeeting.DiscussionEndTime,
            VotingEndTime = _currentMeeting.VotingEndTime
        });
        
        PersistAndBroadcast(evt);
        
        // Zkontroluj všechny hráče, kteří už jsou na místě meetingu
        CheckAllPlayersArrival();
        
        // Spustíme periodickou kontrolu během arrival fáze
        var meetingId = _currentMeeting.MeetingId;
        _ = Task.Run(async () =>
        {
            // Kontrolujeme každou sekundu během arrival fáze
            while (_currentMeeting != null && 
                   _currentMeeting.MeetingId == meetingId && 
                   DateTime.UtcNow < _currentMeeting.ArrivalDeadline)
            {
                await Task.Delay(1000);
                await EnqueueAsync(new CheckMeetingArrivalAction { MeetingId = meetingId });
            }
        });
        
        // Spustíme timer pro voting
        _ = Task.Run(async () =>
        {
            await Task.Delay(_currentMeeting.VotingEndTime - DateTime.UtcNow);
            await EnqueueAsync(new CloseMeetingAction { MeetingId = _currentMeeting.MeetingId });
        });
    }
    
    /// <summary>
    /// Zkontroluje všechny hráče zda jsou na meeting location
    /// </summary>
    private void CheckAllPlayersArrival()
    {
        if (_currentMeeting == null) return;
        
        foreach (var player in _players.Values.Where(p => p.State == PlayerState.Alive))
        {
            if (!_currentMeeting.ArrivedPlayers.Contains(player.ClientUuid) &&
                _gameLogic.CheckPlayerArrival(player, _currentMeeting))
            {
                var arrivalEvt = CreateEvent("PlayerArrivedAtMeeting", player.ClientUuid, new PlayerArrivedAtMeetingPayload
                {
                    ClientUuid = player.ClientUuid,
                    MeetingId = _currentMeeting.MeetingId
                });
                PersistAndBroadcast(arrivalEvt);
            }
        }
    }
    
    private void ProcessCastVote(Player voter, CastVote message, ref Ack ack)
    {
        if (_currentMeeting == null)
        {
            ack.Success = false;
            ack.Error = "Žádný aktivní meeting";
            return;
        }
        
        var (success, error) = _gameLogic.TryCastVote(voter, message.TargetClientUuid, _currentMeeting, _players);
        
        if (!success)
        {
            ack.Success = false;
            ack.Error = error;
            return;
        }
        
        var evt = CreateEvent("PlayerVoted", voter.ClientUuid, new PlayerVotedPayload
        {
            VoterId = voter.ClientUuid,
            TargetId = message.TargetClientUuid
        });
        
        PersistAndBroadcast(evt);
    }
    
    private void CloseMeeting()
    {
        if (_currentMeeting == null) return;
        
        var (ejectedId, wasTie, voteCounts) = _gameLogic.ResolveVoting(_currentMeeting, _players, Settings.TiePolicy);
        
        // Broadcast voting results
        var closeEvt = CreateEvent("VotingClosed", null, new VotingClosedPayload
        {
            VoteCounts = voteCounts,
            EjectedPlayerId = ejectedId,
            WasTie = wasTie
        });
        PersistAndBroadcast(closeEvt);
        
        // Eject player if any
        if (ejectedId != null && _players.TryGetValue(ejectedId, out var ejected))
        {
            _gameLogic.EjectPlayer(ejected);
            _statsDb.IncrementTimesVotedOut(ejectedId);
            
            // Track successful votes (voted for impostor)
            if (ejected.Role == PlayerRole.Impostor)
            {
                foreach (var (voterId, targetId) in _currentMeeting.Votes)
                {
                    if (targetId == ejectedId)
                    {
                        _statsDb.IncrementSuccessfulVotes(voterId);
                    }
                }
            }
            
            var ejectEvt = CreateEvent("PlayerEjected", null, new PlayerEjectedPayload
            {
                ClientUuid = ejectedId,
                Role = ejected.Role
            });
            PersistAndBroadcast(ejectEvt);
        }
        
        // Clear bodies and meeting
        _bodies.Clear();
        _currentMeeting = null;
        _phase = GamePhase.Playing;
        
        // Reset cooldowns for next round
        foreach (var player in _players.Values.Where(p => p.State == PlayerState.Alive))
        {
            if (player.Role == PlayerRole.Impostor)
            {
                player.LastKillTime = DateTime.UtcNow; // Reset kill cooldown
            }
         }
        
        // Check win conditions after ejection
        CheckWinConditions();
    }
    
    private void ProcessTaskComplete(Player player, TaskComplete message, ref Ack ack)
    {
        if (_phase != GamePhase.Playing || _currentMeeting != null)
        {
            ack.Success = false;
            ack.Error = "Nelze provádět tasky";
            return;
        }
        
        if (!_antiCheat.CanPerformAction(player, "TaskComplete"))
        {
            ack.Success = false;
            ack.Error = "Akce blokována";
            return;
        }
        
        var (success, error) = _gameLogic.TryCompleteTask(player, message.TaskId);
        
        if (!success)
        {
            ack.Success = false;
            ack.Error = error;
            return;
        }
        
        // Task dokončen úspěšně
        _statsDb.IncrementTasksCompleted(player.ClientUuid);
        
        var crewPlayers = _players.Values.Where(p => p.Role == PlayerRole.Crew).ToList();
        var totalTasks = crewPlayers.Sum(p => p.Tasks.Count);
        var completedTasks = crewPlayers.Sum(p => p.CompletedTaskIds.Count);
        
        var evt = CreateEvent("TaskCompleted", player.ClientUuid, new TaskCompletedPayload
        {
            ClientUuid = player.ClientUuid,
            TaskId = message.TaskId,
            TotalCompleted = completedTasks,
            TotalTasks = totalTasks
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Task {TaskId} completed by {Player}. Progress: {Completed}/{Total}", 
            message.TaskId, player.DisplayName, completedTasks, totalTasks);
        
        CheckWinConditions();
    }

    private void ProcessKickPlayer(Player kicker, KickPlayer message, ref Ack ack)
    {
        if (!kicker.IsOwner)
        {
            ack.Success = false;
            ack.Error = "Pouze owner může kicknout";
            return;
        }
        
        if (message.TargetClientUuid == kicker.ClientUuid)
        {
            ack.Success = false;
            ack.Error = "Nemůžeš kicknout sám sebe";
            return;
        }
        
        _ = RemovePlayerAsync(message.TargetClientUuid, "Kicked by owner");
    }
    
    private void CheckWinConditions()
    {
        var (gameOver, winningFaction, reason) = _gameLogic.CheckWinConditions(_players, _tasks);
        
        if (gameOver)
        {
            EndGame(winningFaction!, reason!);
        }
    }
    
    private void EndGame(string winningFaction, string reason)
    {
        _phase = GamePhase.Ended;
        
        var winners = _players.Values
            .Where(p => (winningFaction == "Crew" && p.Role == PlayerRole.Crew) ||
                        (winningFaction == "Impostor" && p.Role == PlayerRole.Impostor))
            .Select(p => p.ClientUuid)
            .ToList();
        
        var evt = CreateEvent("GameEnded", null, new GameEndedPayload
        {
            WinningFaction = winningFaction,
            Reason = reason,
            Winners = winners
        });
        
        PersistAndBroadcast(evt);
        
        // Uložíme statistiky
        var gameStats = new GameEndStats
        {
            CrewWon = winningFaction == "Crew",
            Players = _players.Values.Select(p => new PlayerGameStats
            {
                ClientUuid = p.ClientUuid,
                WasCrew = p.Role == PlayerRole.Crew,
                WasImpostor = p.Role == PlayerRole.Impostor,
                PlaytimeSeconds = (int)(DateTime.UtcNow - p.ConnectedAt).TotalSeconds
            }).ToList()
        };
        
        _statsDb.RecordGameEnd(gameStats);
        
        _logger.LogInformation("Hra ukončena: {Winner} vyhráli - {Reason}", winningFaction, reason);
    }
    
    private void ProcessReturnToLobby(Player player, ref Ack ack)
    {
        // Pouze owner může restartovat hru
        if (!player.IsOwner)
        {
            ack.Success = false;
            ack.Error = "Pouze owner může restartovat hru";
            return;
        }
        
        // Pouze pokud je hra ve fázi Ended
        if (_phase != GamePhase.Ended)
        {
            ack.Success = false;
            ack.Error = "Hra ještě neskončila";
            return;
        }
        
        // Resetujeme stav hry
        _phase = GamePhase.Lobby;
        _bodies.Clear();
        _tasks.Clear();
        _currentMeeting = null;
        _currentSabotage = null;
        _playersWithMapData.Clear();
        
        // Resetujeme hráče
        foreach (var p in _players.Values)
        {
            p.Role = PlayerRole.Crew;
            p.Position = Settings.PlayAreaCenter;
            p.State = PlayerState.Alive;
            p.Tasks.Clear();
            p.CompletedTaskIds.Clear();
            p.CurrentTaskId = null;
        }
        
        // Broadcastujeme událost
        var evt = CreateEvent("ReturnedToLobby", player.ClientUuid, new ReturnedToLobbyPayload
        {
            Message = "Hra byla restartována"
        });
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Lobby {LobbyId} vráceno do lobby fáze hráčem {Player}", _lobbyId, player.DisplayName);
    }
    
    #endregion
    
    #region Events & Broadcasting
    
    private GameEvent CreateEvent(string eventType, string? actor, object? payload)
    {
        return new GameEvent
        {
            EventId = Interlocked.Increment(ref _eventId),
            ServerSeq = Interlocked.Increment(ref _serverSeq),
            Timestamp = DateTime.UtcNow,
            EventType = eventType,
            Actor = actor,
            Payload = payload
        };
    }
    
    private void PersistEvent(GameEvent evt)
    {
        _persistence.AppendToWal(_lobbyId, evt);
        _snapshotEventCounter++;
    }
    
    private void PersistAndBroadcast(GameEvent evt)
    {
        PersistEvent(evt);
        BroadcastEvent(evt);
    }
    
    private void BroadcastEvent(GameEvent evt)
    {
        var message = new GameEvent
        {
            EventId = evt.EventId,
            ServerSeq = evt.ServerSeq,
            Timestamp = evt.Timestamp,
            EventType = evt.EventType,
            Actor = evt.Actor,
            Payload = evt.Payload
        };
        
        foreach (var (clientUuid, connection) in _connections)
        {
            try
            {
                connection.SendAsync(message).Wait();
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Chyba při broadcastu do {ClientUuid}", clientUuid);
            }
        }
    }
    
    private void SendTo(string clientUuid, Message message)
    {
        if (_connections.TryGetValue(clientUuid, out var connection))
        {
            try
            {
                connection.SendAsync(message).Wait();
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Chyba při odesílání do {ClientUuid}", clientUuid);
            }
        }
    }
    
    // Position broadcast - volat periodicky z externího timeru
    public void BroadcastPositions()
    {
        if (_phase != GamePhase.Playing && _phase != GamePhase.Meeting)
            return;
            
        var positions = _players.Values.Select(p => new PlayerPositionInfo
        {
            ClientUuid = p.ClientUuid,
            Position = p.Position,
            State = p.State
        }).ToList();
        
        var broadcast = new PositionBroadcast { Players = positions };
        
        foreach (var connection in _connections.Values)
        {
            try
            {
                connection.SendAsync(broadcast).Wait();
            }
            catch { }
        }
    }
    
    #endregion
    
    #region Sabotage
    
    private void ProcessStartSabotage(Player player, StartSabotage message, ref Ack ack)
    {
        // Only impostors can sabotage
        if (player.Role != PlayerRole.Impostor)
        {
            ack.Success = false;
            ack.Error = "Pouze impostor může sabotovat";
            return;
        }
        
        // Must be in playing phase (not meeting)
        if (_phase != GamePhase.Playing)
        {
            ack.Success = false;
            ack.Error = "Nelze sabotovat během meetingu";
            return;
        }
        
        // Check cooldown
        if (_lastSabotageTime.HasValue && 
            (DateTime.UtcNow - _lastSabotageTime.Value).TotalMilliseconds < _config.SabotageCooldownMs)
        {
            var remaining = _config.SabotageCooldownMs - (DateTime.UtcNow - _lastSabotageTime.Value).TotalMilliseconds;
            ack.Success = false;
            ack.Error = $"Sabotáž na cooldownu ({remaining:F0}ms)";
            return;
        }
        
        // Check if sabotage already active
        if (_currentSabotage != null && _currentSabotage.State == SabotageState.Active)
        {
            ack.Success = false;
            ack.Error = "Již probíhá jiná sabotáž";
            return;
        }
        
        // Create sabotage
        _currentSabotage = new Sabotage
        {
            SabotageId = Guid.NewGuid().ToString("N")[..8],
            Type = message.SabotageType,
            InitiatorId = player.ClientUuid,
            StartTime = DateTime.UtcNow
        };
        
        // Setup based on type
        switch (message.SabotageType)
        {
            case SabotageType.CommsBlackout:
                SetupCommsBlackout(_currentSabotage);
                break;
            case SabotageType.CriticalMeltdown:
                SetupCriticalMeltdown(_currentSabotage);
                break;
        }
        
        _lastSabotageTime = DateTime.UtcNow;
        
        // Broadcast sabotage started
        var evt = CreateEvent("SabotageStarted", player.ClientUuid, new SabotageStartedPayload
        {
            SabotageId = _currentSabotage.SabotageId,
            Type = _currentSabotage.Type,
            InitiatorId = _currentSabotage.InitiatorId,
            Deadline = _currentSabotage.Deadline,
            RepairStations = _currentSabotage.RepairStations.Select(s => new RepairStationInfo
            {
                StationId = s.StationId,
                Name = s.Name,
                Location = s.Location,
                RepairDurationMs = s.RepairDurationMs
            }).ToList(),
            RequiredSimultaneousRepairs = _currentSabotage.RequiredSimultaneousRepairs
        });
        
        PersistAndBroadcast(evt);
        
        // Start timer for auto-expire or meltdown
        _ = Task.Run(async () =>
        {
            if (_currentSabotage.Deadline.HasValue)
            {
                // Critical meltdown - wait until deadline
                await Task.Delay(_currentSabotage.Deadline.Value - DateTime.UtcNow);
                await EnqueueAsync(new SabotageMeltdownAction { SabotageId = _currentSabotage.SabotageId });
            }
            else
            {
                // Comms blackout - auto-expire
                await Task.Delay(_config.CommsBlackoutDurationMs);
                await EnqueueAsync(new SabotageExpireAction { SabotageId = _currentSabotage.SabotageId });
            }
        });
        
        _logger.LogInformation("Sabotáž spuštěna: {Type} by {Player}", message.SabotageType, player.DisplayName);
    }
    
    private void SetupCommsBlackout(Sabotage sabotage)
    {
        sabotage.RequiredSimultaneousRepairs = 1;
        
        // Get a reachable position using map data if available
        var positions = _gameLogic.GenerateRepairStationPositions(
            1, Settings.PlayAreaCenter, Settings.PlayAreaRadius, Settings.MapData);
        
        var station = new RepairStation
        {
            StationId = "comms_station",
            Name = "Komunikační věž",
            Location = positions.FirstOrDefault(),
            RepairDurationMs = _config.RepairStationHoldMs
        };
        
        sabotage.RepairStations.Add(station);
    }
    
    private void SetupCriticalMeltdown(Sabotage sabotage)
    {
        sabotage.Deadline = DateTime.UtcNow.AddMilliseconds(_config.CriticalMeltdownDeadlineMs);
        sabotage.RequiredSimultaneousRepairs = 2;
        
        // Two repair stations - use map data for reachable positions if available
        var positions = _gameLogic.GenerateRepairStationPositions(
            2, Settings.PlayAreaCenter, Settings.PlayAreaRadius, Settings.MapData);
        
        var station1 = new RepairStation
        {
            StationId = "reactor_alpha",
            Name = "Reaktor Alpha",
            Location = positions.Count > 0 ? positions[0] : Settings.PlayAreaCenter,
            RepairDurationMs = _config.RepairStationHoldMs
        };
        
        var station2 = new RepairStation
        {
            StationId = "reactor_beta",
            Name = "Reaktor Beta",
            Location = positions.Count > 1 ? positions[1] : Settings.PlayAreaCenter,
            RepairDurationMs = _config.RepairStationHoldMs
        };
        
        sabotage.RepairStations.Add(station1);
        sabotage.RepairStations.Add(station2);
    }
    
    private Position GenerateRandomLocation()
    {
        // Try to use map data for reachable position
        if (Settings.MapData != null && _overpassService != null)
        {
            var pos = _overpassService.GetRandomReachablePosition(Settings.MapData, new Random());
            if (pos.HasValue)
            {
                return pos.Value;
            }
        }
        
        // Fallback to random position
        var center = Settings.PlayAreaCenter;
        var radius = Settings.PlayAreaRadius * 0.6; // 60% of play area
        
        var random = new Random();
        var angle = random.NextDouble() * 2 * Math.PI;
        var dist = random.NextDouble() * radius;
        
        var latOffset = (dist * Math.Cos(angle)) / 111000.0;
        var lonOffset = (dist * Math.Sin(angle)) / (111000.0 * Math.Cos(center.Lat * Math.PI / 180));
        
        return new Position(center.Lat + latOffset, center.Lon + lonOffset);
    }
    
    private void ProcessActivateRepairStation(Player player, ActivateRepairStation message, ref Ack ack)
    {
        if (_currentSabotage == null || _currentSabotage.State != SabotageState.Active)
        {
            ack.Success = false;
            ack.Error = "Žádná aktivní sabotáž";
            return;
        }
        
        // Dead players can't repair
        if (player.State != PlayerState.Alive)
        {
            ack.Success = false;
            ack.Error = "Mrtví hráči nemohou opravovat";
            return;
        }
        
        var station = _currentSabotage.RepairStations.FirstOrDefault(s => s.StationId == message.StationId);
        if (station == null)
        {
            ack.Success = false;
            ack.Error = "Neznámá stanice";
            return;
        }
        
        // Check distance
        var distance = player.Position.DistanceTo(station.Location);
        if (distance > _config.RepairStationDistanceM)
        {
            ack.Success = false;
            ack.Error = $"Příliš daleko od stanice ({distance:F1}m)";
            return;
        }
        
        // INSTANT oprava - klient může simulovat progress bar lokálně
        station.IsRepaired = true;
        station.RepairingPlayerId = player.ClientUuid;
        station.RepairStartTime = DateTime.UtcNow;
        
        _logger.LogInformation("Stanice opravena: {Station} by {Player}", station.Name, player.DisplayName);
        
        // Zkontroluj zda je sabotáž kompletně opravena
        CheckSabotageRepairComplete(player.ClientUuid, station.StationId);
    }
    
    private void ProcessDeactivateRepairStation(Player player, DeactivateRepairStation message, ref Ack ack)
    {
        if (_currentSabotage == null)
        {
            ack.Success = false;
            ack.Error = "Žádná aktivní sabotáž";
            return;
        }
        
        var station = _currentSabotage.RepairStations.FirstOrDefault(s => s.StationId == message.StationId);
        if (station == null)
        {
            ack.Success = false;
            ack.Error = "Neznámá stanice";
            return;
        }
        
        if (station.RepairingPlayerId != player.ClientUuid)
        {
            ack.Success = false;
            ack.Error = "Tuto stanici neopravujete";
            return;
        }
        
        // Stop repairing
        station.IsBeingRepaired = false;
        station.RepairingPlayerId = null;
        station.RepairStartTime = null;
        
        var evt = CreateEvent("RepairStopped", player.ClientUuid, new RepairStoppedPayload
        {
            SabotageId = _currentSabotage.SabotageId,
            StationId = station.StationId,
            PlayerId = player.ClientUuid
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Oprava přerušena: {Station} by {Player}", station.Name, player.DisplayName);
    }
    
    private void ProcessRepairComplete(RepairCompleteAction action)
    {
        if (_currentSabotage == null || _currentSabotage.SabotageId != action.SabotageId)
            return;
            
        if (_currentSabotage.State != SabotageState.Active)
            return;
        
        var station = _currentSabotage.RepairStations.FirstOrDefault(s => s.StationId == action.StationId);
        if (station == null || station.RepairingPlayerId != action.PlayerId)
            return;
        
        // Check if player is still near the station
        if (_players.TryGetValue(action.PlayerId, out var player))
        {
            var distance = player.Position.DistanceTo(station.Location);
            if (distance > _config.RepairStationDistanceM)
            {
                // Player moved away, cancel repair
                station.IsBeingRepaired = false;
                station.RepairingPlayerId = null;
                station.RepairStartTime = null;
                return;
            }
        }
        
        // Mark station as repair complete (keep IsBeingRepaired true for multi-station check)
        station.IsRepaired = true;
        
        // Check repair completion based on sabotage type
        if (_currentSabotage.RequiredSimultaneousRepairs == 1)
        {
            // Simple single-station repair
            station.IsBeingRepaired = false;
            CompleteSabotageRepair(new List<string> { action.PlayerId });
        }
        else
        {
            // Multi-station simultaneous repair
            var repairedStations = _currentSabotage.RepairStations
                .Where(s => s.IsRepaired && s.RepairStartTime.HasValue)
                .ToList();
                
            if (repairedStations.Count >= _currentSabotage.RequiredSimultaneousRepairs)
            {
                // Check if repairs completed within the time window
                var repairCompleteTimes = repairedStations
                    .Select(s => s.RepairStartTime!.Value.AddMilliseconds(s.RepairDurationMs))
                    .OrderBy(t => t)
                    .ToList();
                
                var timeDiff = (repairCompleteTimes.Last() - repairCompleteTimes.First()).TotalMilliseconds;
                
                if (timeDiff <= _config.SimultaneousRepairWindowMs)
                {
                    // Simultaneous repair successful
                    var repairerIds = repairedStations
                        .Where(s => s.RepairingPlayerId != null)
                        .Select(s => s.RepairingPlayerId!)
                        .ToList();
                    
                    // Clear repair states
                    foreach (var s in _currentSabotage.RepairStations)
                    {
                        s.IsBeingRepaired = false;
                    }
                    
                    CompleteSabotageRepair(repairerIds);
                }
            }
        }
    }
    
    private void CheckSabotageRepairComplete(string playerId, string stationId)
    {
        if (_currentSabotage == null) return;
        
        // Check repair completion based on sabotage type
        if (_currentSabotage.RequiredSimultaneousRepairs == 1)
        {
            // Simple single-station repair - done!
            CompleteSabotageRepair(new List<string> { playerId });
        }
        else
        {
            // Multi-station repair - check if all stations are repaired
            var repairedStations = _currentSabotage.RepairStations.Where(s => s.IsRepaired).ToList();
            
            if (repairedStations.Count >= _currentSabotage.RequiredSimultaneousRepairs)
            {
                // Check if repairs completed within the time window
                var repairTimes = repairedStations
                    .Where(s => s.RepairStartTime.HasValue)
                    .Select(s => s.RepairStartTime!.Value)
                    .OrderBy(t => t)
                    .ToList();
                
                if (repairTimes.Count >= 2)
                {
                    var timeDiff = (repairTimes.Last() - repairTimes.First()).TotalMilliseconds;
                    
                    if (timeDiff <= _config.SimultaneousRepairWindowMs)
                    {
                        // Simultaneous repair successful!
                        var repairerIds = repairedStations
                            .Where(s => s.RepairingPlayerId != null)
                            .Select(s => s.RepairingPlayerId!)
                            .ToList();
                        
                        CompleteSabotageRepair(repairerIds);
                    }
                    else
                    {
                        // Too slow - reset the older repairs
                        foreach (var s in _currentSabotage.RepairStations)
                        {
                            if (s.StationId != stationId)
                            {
                                s.IsRepaired = false;
                                s.RepairingPlayerId = null;
                                s.RepairStartTime = null;
                            }
                        }
                        _logger.LogInformation("Opravy nebyly simultánní - reset starších oprav");
                    }
                }
            }
        }
    }
    
    private void CompleteSabotageRepair(List<string> repairerIds)
    {
        if (_currentSabotage == null) return;
        
        _currentSabotage.State = SabotageState.Repaired;
        _currentSabotage.RepairedAt = DateTime.UtcNow;
        
        var evt = CreateEvent("SabotageRepaired", repairerIds.First(), new SabotageRepairedPayload
        {
            SabotageId = _currentSabotage.SabotageId,
            Type = _currentSabotage.Type,
            RepairerIds = repairerIds
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Sabotáž opravena: {Type} by {Players}", 
            _currentSabotage.Type, 
            string.Join(", ", repairerIds));
        
        _currentSabotage = null;
    }
    
    private void ProcessSabotageMeltdown(SabotageMeltdownAction action)
    {
        if (_currentSabotage == null || _currentSabotage.SabotageId != action.SabotageId)
            return;
            
        if (_currentSabotage.State != SabotageState.Active)
            return;
        
        // Meltdown - impostors win!
        var meltdownEvt = CreateEvent("SabotageMeltdown", _currentSabotage.InitiatorId, new SabotageMeltdownPayload
        {
            SabotageId = _currentSabotage.SabotageId
        });
        
        PersistAndBroadcast(meltdownEvt);
        
        // End game - impostor win
        EndGame("Impostor", "Kritická sabotáž - meltdown");
    }
    
    private void ProcessSabotageExpire(SabotageExpireAction action)
    {
        if (_currentSabotage == null || _currentSabotage.SabotageId != action.SabotageId)
            return;
            
        if (_currentSabotage.State != SabotageState.Active)
            return;
        
        // Comms blackout expired naturally
        _currentSabotage.State = SabotageState.Repaired;
        _currentSabotage = null;
        
        var evt = CreateEvent("SabotageExpired", "system", new SabotageRepairedPayload
        {
            SabotageId = action.SabotageId,
            Type = SabotageType.CommsBlackout,
            RepairerIds = new List<string>()
        });
        
        PersistAndBroadcast(evt);
        
        _logger.LogInformation("Sabotáž vypršela: CommsBlackout");
    }
    
    /// <summary>
    /// Check if comms sabotage blocks reporting/meetings
    /// </summary>
    public bool IsCommsBlocked()
    {
        return _currentSabotage != null && 
               _currentSabotage.Type == SabotageType.CommsBlackout && 
               _currentSabotage.State == SabotageState.Active;
    }
    
    /// <summary>
    /// Check if reactor meltdown blocks reporting/meetings
    /// </summary>
    public bool IsMeltdownActive()
    {
        return _currentSabotage != null && 
               _currentSabotage.Type == SabotageType.CriticalMeltdown && 
               _currentSabotage.State == SabotageState.Active;
    }
    
    /// <summary>
    /// Get current sabotage info (for state sync)
    /// </summary>
    public Sabotage? GetCurrentSabotage() => _currentSabotage;
    
    #endregion
    
    #region Snapshots
    
    private void CheckSnapshot()
    {
        var shouldSnapshot = _snapshotEventCounter >= _config.SnapshotEvents ||
                            (DateTime.UtcNow - _lastSnapshotTime).TotalMilliseconds >= _config.SnapshotIntervalMs;
                            
        if (shouldSnapshot)
        {
            SaveSnapshot();
        }
    }
    
    private void SaveSnapshot()
    {
        var snapshot = new LobbySnapshot
        {
            LobbyId = _lobbyId,
            LastEventId = _eventId,
            Phase = _phase,
            Players = _players.Values.ToList(),
            Bodies = _bodies.ToList(),
            Tasks = _tasks.ToList(),
            CurrentMeeting = _currentMeeting,
            PlayAreaCenter = Settings.PlayAreaCenter,
            PlayAreaRadius = Settings.PlayAreaRadius,
            ImpostorCount = Settings.ImpostorCount,
            TiePolicy = Settings.TiePolicy,
            Checksum = "" // Bude vyplněno v Persistence
        };
        
        _persistence.SaveSnapshot(_lobbyId, snapshot);
        _snapshotEventCounter = 0;
        _lastSnapshotTime = DateTime.UtcNow;
    }
    
    #endregion
    
    public void Dispose()
    {
        _cts.Cancel();
        _cts.Dispose();
    }
}

#region Action types

abstract class LobbyAction 
{ 
    public TaskCompletionSource? Completion { get; set; }
}

class AddPlayerAction : LobbyAction
{
    public required string ClientUuid { get; set; }
    public required string DisplayName { get; set; }
    public bool IsOwner { get; set; }
}

class RemovePlayerAction : LobbyAction
{
    public required string ClientUuid { get; set; }
    public string? Reason { get; set; }
}

class HandleMessageAction : LobbyAction
{
    public required string ClientUuid { get; set; }
    public required Message Message { get; set; }
}

class CloseMeetingAction : LobbyAction
{
    public required string MeetingId { get; set; }
}

class CheckMeetingArrivalAction : LobbyAction
{
    public required string MeetingId { get; set; }
}

class RepairCompleteAction : LobbyAction
{
    public required string SabotageId { get; set; }
    public required string StationId { get; set; }
    public required string PlayerId { get; set; }
}

class SabotageMeltdownAction : LobbyAction
{
    public required string SabotageId { get; set; }
}

class SabotageExpireAction : LobbyAction
{
    public required string SabotageId { get; set; }
}

#endregion

// Stub pro ClientConnection - bude implementováno v Program.cs
public class ClientConnection
{
    public required string ClientUuid { get; set; }
    public required Func<Message, Task> SendAsync { get; set; }
}

// Extension metoda pro Dictionary
public static class DictionaryExtensions
{
    public static bool TryRemove<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, out TValue? value) where TKey : notnull
    {
        if (dict.TryGetValue(key, out value))
        {
            dict.Remove(key);
            return true;
        }
        value = default;
        return false;
    }
}

// Admin panel DTOs
public class AdminPlayerInfo
{
    public required string PlayerId { get; set; }
    public required string DisplayName { get; set; }
    public required PlayerState State { get; set; }
    public required PlayerRole Role { get; set; }
    public required Position Position { get; set; }
    public bool IsHost { get; set; }
    public int CheatScore { get; set; }
    public DateTime? KillCooldownEnd { get; set; }
    public string? VotedFor { get; set; }
}

public class AdminTaskInfo
{
    public required string TaskId { get; set; }
    public required string Name { get; set; }
    public required Position Location { get; set; }
    public HashSet<string> CompletedBy { get; set; } = new();
}

public class AdminBodyInfo
{
    public required string VictimId { get; set; }
    public required Position Position { get; set; }
    public DateTime? ReportedAt { get; set; }
}

public class SystemMessagePayload
{
    public required string Message { get; set; }
    public DateTime Timestamp { get; set; }
}
