using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace GeoSus.Client
{
    // Hlavní klientská třída pro připojení k serveru
public class GameClient : IDisposable
{
    private TcpClient? _tcpClient;
    private NetworkStream? _stream;
    private ClientEncryption? _encryption;
    private CancellationTokenSource? _cts;
    private Task? _receiveTask;
    private int _clientSeq;
    private readonly object _sendLock = new object();
    private bool _handshakeComplete;
    
    public string ClientUuid { get; }
    public string DisplayName { get; set; }
    public bool IsConnected => _tcpClient?.Connected ?? false;
    public bool IsReady => IsConnected && _handshakeComplete && (_encryption?.HasSessionKey ?? false);
    public EventDispatcher Dispatcher { get; }
    
    // Events - voláno na main thread přes dispatcher
    public event Action? OnConnected;
    public event Action<string>? OnDisconnected;
    public event Action<string>? OnError;
    public event Action<Message>? OnMessage;
    public event Action<GameEvent>? OnGameEvent;
    
    // Lobby state
    public string? LobbyId { get; private set; }
    public string? JoinCode { get; private set; }
    public LobbyState? CurrentLobbyState { get; private set; }
    public PlayerRole? MyRole { get; private set; }
    public List<GameTask> MyTasks { get; } = new List<GameTask>();
    public Position MyPosition { get; set; }
    public Dictionary<string, PlayerPositionInfo> PlayerPositions { get; } = new Dictionary<string, PlayerPositionInfo>();
    public List<Body> Bodies { get; } = new List<Body>();
    public int Ping { get; private set; }
    public long LastEventId { get; private set; }
    
    /// <summary>Returns true if this client is the current lobby owner</summary>
    public bool IsOwner => CurrentLobbyState?.OwnerId == ClientUuid;
    
    public GameClient(string clientUuid, string displayName)
    {
        ClientUuid = clientUuid;
        DisplayName = displayName;
        Dispatcher = new EventDispatcher();
    }
    
    #region Connection
    
    public async Task<bool> ConnectAsync(string host, int port)
    {
        try
        {
            _tcpClient = new TcpClient();
            await _tcpClient.ConnectAsync(host, port);
            _stream = _tcpClient.GetStream();
            _encryption = new ClientEncryption();
            _cts = new CancellationTokenSource();
            
            // Handshake
            if (!await PerformHandshakeAsync())
            {
                Disconnect("Handshake failed");
                return false;
            }
            
            // Spustíme příjem zpráv
            _receiveTask = Task.Run(() => ReceiveLoopAsync(_cts.Token));
            
            Dispatcher.Post(() => OnConnected?.Invoke());
            return true;
        }
        catch (Exception ex)
        {
            Dispatcher.Post(() => OnError?.Invoke(ex.Message));
            return false;
        }
    }
    
    private async Task<bool> PerformHandshakeAsync()
    {
        if (_stream == null || _encryption == null) return false;
        
        // 1. ClientHello
        var hello = new ClientHello
        {
            ClientUuid = ClientUuid,
            DisplayName = DisplayName
        };
        await SendPlainAsync(hello);
        
        // 2. ServerHello
        var serverHelloData = await ReadMessageAsync();
        if (serverHelloData == null) return false;
        
        var serverHello = MessageSerializer.Deserialize(serverHelloData) as ServerHello;
        if (serverHello == null) return false;
        
        // 3. Generujeme session key a šifrujeme RSA
        _encryption.GenerateSessionKey();
        var (encKey, encIv) = _encryption.EncryptSessionKeyForServer(serverHello.RsaPublicKeyPem);
        
        var keyExchange = new KeyExchange
        {
            EncryptedSessionKey = encKey,
            EncryptedIV = encIv
        };
        await SendPlainAsync(keyExchange);
        
        // 4. KeyExchangeAck (šifrovaně)
        var ackData = await ReadMessageAsync();
        if (ackData == null) return false;
        
        var decrypted = _encryption.Decrypt(ackData);
        if (decrypted == null) return false;
        
        var ack = MessageSerializer.Deserialize(decrypted) as KeyExchangeAck;
        if (ack?.Status == "success")
        {
            _handshakeComplete = true;
            return true;
        }
        return false;
    }
    
    /// <summary>
    /// Tears down the socket and crypto session. When `transient` is true
    /// (network drop, decrypt-failure cascade, anything we expect to retry),
    /// the lobby/role/task/state caches are preserved so the post-reconnect
    /// flow can re-associate via Reconnect(LobbyId). Default false matches
    /// pre-P9 behavior (full state wipe) for explicit user disconnects.
    ///
    /// Critical for the P9 reconnect bug: previously every Disconnect path
    /// nuked LobbyId, so by the time GameManager_Network's reconnect coroutine
    /// fired, the client had no idea which lobby it had been in - the
    /// post-handshake Reconnect call had nothing to send and the server
    /// answered the next vote/action with NOT_IN_LOBBY.
    /// </summary>
    public void Disconnect(string reason = "User disconnected", bool transient = false)
    {
        _cts?.Cancel();
        _tcpClient?.Close();
        _tcpClient = null;
        _stream = null;
        _encryption?.Dispose();
        _encryption = null;

        if (!transient)
        {
            LobbyId = null;
            JoinCode = null;
            CurrentLobbyState = null;
            MyRole = null;
            MyTasks.Clear();
            PlayerPositions.Clear();
            Bodies.Clear();
        }
        // PlayerPositions are stale anyway after a drop, but we keep them so
        // the UI doesn't blink avatars off-map mid-meeting; the next position
        // broadcast overwrites them. LastEventId is intentionally preserved
        // so the Reconnect message can ask the server for missed events.

        Dispatcher.Post(() => OnDisconnected?.Invoke(reason));
    }
    
    #endregion
    
    #region Sending
    
    public void Send(Message message)
    {
        if (_stream == null || _encryption == null || !IsConnected) return;
        
        message.ClientSeq = Interlocked.Increment(ref _clientSeq);
        if (string.IsNullOrEmpty(message.ActionId))
        {
            message.ActionId = Guid.NewGuid().ToString("N").Substring(0, 8);
        }
        
        var plain = MessageSerializer.Serialize(message);
        var encrypted = _encryption.Encrypt(plain);
        
        lock (_sendLock)
        {
            try
            {
                SendData(encrypted);
            }
            catch (Exception ex)
            {
                Dispatcher.Post(() => OnError?.Invoke($"Send error: {ex.Message}"));
            }
        }
    }
    
    private async Task SendPlainAsync(Message message)
    {
        if (_stream == null) return;
        var data = MessageSerializer.Serialize(message);
        await SendDataAsync(data);
    }
    
    private void SendData(byte[] data)
    {
        if (_stream == null) return;
        
        var lengthBuffer = BitConverter.GetBytes(data.Length);
        if (BitConverter.IsLittleEndian)
            Array.Reverse(lengthBuffer);
            
        _stream.Write(lengthBuffer, 0, 4);
        _stream.Write(data, 0, data.Length);
        _stream.Flush();
    }
    
    private async Task SendDataAsync(byte[] data)
    {
        if (_stream == null) return;
        
        var lengthBuffer = BitConverter.GetBytes(data.Length);
        if (BitConverter.IsLittleEndian)
            Array.Reverse(lengthBuffer);
            
        await _stream.WriteAsync(lengthBuffer, 0, 4);
        await _stream.WriteAsync(data, 0, data.Length);
        await _stream.FlushAsync();
    }
    
    #endregion
    
    #region Receiving
    
    private async Task ReceiveLoopAsync(CancellationToken ct)
    {
        int decryptFailures = 0;
        
        try
        {
            while (!ct.IsCancellationRequested && IsConnected)
            {
                var data = await ReadMessageAsync();
                if (data == null) break;
                
                var decrypted = _encryption?.Decrypt(data);
                if (decrypted == null)
                {
                    decryptFailures++;
                    if (decryptFailures >= 3)
                    {
                        // Transient: keep LobbyId for the reconnect coroutine.
                        Disconnect("Too many decryption failures", transient: true);
                        return;
                    }
                    continue;
                }
                
                decryptFailures = 0;
                
                var message = MessageSerializer.Deserialize(decrypted);
                if (message != null)
                {
                    ProcessMessage(message);
                }
            }
        }
        catch (Exception ex) when (!ct.IsCancellationRequested)
        {
            // Transient: TCP RST / read failure is exactly what reconnect was
            // designed for. Keep LobbyId so post-reconnect flow can re-attach.
            Disconnect($"Connection error: {ex.Message}", transient: true);
        }
    }
    
    private async Task<byte[]?> ReadMessageAsync()
    {
        if (_stream == null) return null;
        
        var lengthBuffer = new byte[4];
        var read = await _stream.ReadAsync(lengthBuffer, 0, 4);
        if (read < 4) return null;
        
        if (BitConverter.IsLittleEndian)
            Array.Reverse(lengthBuffer);
        var length = BitConverter.ToInt32(lengthBuffer, 0);
        
        if (length <= 0 || length > 1048576) return null;
        
        var buffer = new byte[length];
        var totalRead = 0;
        while (totalRead < length)
        {
            read = await _stream.ReadAsync(buffer, totalRead, length - totalRead);
            if (read == 0) return null;
            totalRead += read;
        }
        
        return buffer;
    }
    
    private void ProcessMessage(Message message)
    {
        // Zpracujeme speciální typy
        switch (message)
        {
            case CreateLobbyResponse r:
                if (r.Success)
                {
                    LobbyId = r.LobbyId;
                    JoinCode = r.JoinCode;
                    // Ensure we always have a valid lobby state with the creator as owner
                    if (r.LobbyState != null)
                    {
                        CurrentLobbyState = r.LobbyState;
                        if (string.IsNullOrEmpty(CurrentLobbyState.OwnerId))
                            CurrentLobbyState.OwnerId = ClientUuid;
                    }
                    else
                    {
                        CurrentLobbyState = new LobbyState
                        {
                            LobbyId = r.LobbyId ?? "",
                            JoinCode = r.JoinCode ?? "",
                            OwnerId = ClientUuid
                        };
                    }
                    // Make sure creator appears in the player list
                    if (CurrentLobbyState.Players == null)
                        CurrentLobbyState.Players = new System.Collections.Generic.List<PlayerInfo>();
                    if (!CurrentLobbyState.Players.Any(p => p.ClientUuid == ClientUuid))
                    {
                        CurrentLobbyState.Players.Insert(0, new PlayerInfo
                        {
                            ClientUuid = ClientUuid,
                            DisplayName = DisplayName,
                            IsOwner = true,
                            State = PlayerState.Alive
                        });
                    }
                }
                break;
                
            case JoinLobbyResponse r:
                if (r.Success)
                {
                    LobbyId = r.LobbyId;
                    CurrentLobbyState = r.LobbyState;
                    JoinCode = r.LobbyState?.JoinCode;
                    // Ensure self is in the player list
                    if (CurrentLobbyState != null)
                    {
                        if (CurrentLobbyState.Players == null)
                            CurrentLobbyState.Players = new System.Collections.Generic.List<PlayerInfo>();
                        if (!CurrentLobbyState.Players.Any(p => p.ClientUuid == ClientUuid))
                        {
                            CurrentLobbyState.Players.Add(new PlayerInfo
                            {
                                ClientUuid = ClientUuid,
                                DisplayName = DisplayName,
                                IsOwner = CurrentLobbyState.OwnerId == ClientUuid,
                                State = PlayerState.Alive
                            });
                        }
                    }
                }
                break;
                
            case PositionBroadcast b:
                ProcessPositionBroadcast(b);
                break;
                
            case Pong p:
                var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                Ping = (int)(now - p.ClientTime);
                break;
                
            case GameEvent evt:
                ProcessGameEvent(evt);
                Dispatcher.Post(() => OnGameEvent?.Invoke(evt));
                break;
        }
        
        Dispatcher.Post(() => OnMessage?.Invoke(message));
    }
    
    private void ProcessPositionBroadcast(PositionBroadcast broadcast)
    {
        PlayerPositions.Clear();
        foreach (var player in broadcast.Players)
        {
            PlayerPositions[player.ClientUuid] = player;
        }
    }
    
    private void ProcessGameEvent(GameEvent evt)
    {
        LastEventId = evt.EventId;
        
        switch (evt.EventType)
        {
            case "PlayerJoined":
                // Add player to lobby state
                var joinedPayload = evt.GetPayload<PlayerJoinedPayload>();
                if (joinedPayload != null && CurrentLobbyState?.Players != null)
                {
                    bool exists = CurrentLobbyState.Players.Any(p => p.ClientUuid == joinedPayload.ClientUuid);
                    if (!exists)
                    {
                        CurrentLobbyState.Players.Add(new PlayerInfo
                        {
                            ClientUuid = joinedPayload.ClientUuid,
                            DisplayName = joinedPayload.DisplayName,
                            IsOwner = joinedPayload.ClientUuid == CurrentLobbyState.OwnerId,
                            IsReady = false,
                            State = PlayerState.Alive
                        });
                    }
                }
                break;
                
            case "PlayerLeft":
                // Remove player from lobby state
                var leftPayload = evt.GetPayload<PlayerLeftPayload>();
                if (leftPayload != null && CurrentLobbyState?.Players != null)
                {
                    CurrentLobbyState.Players.RemoveAll(p => p.ClientUuid == leftPayload.ClientUuid);
                }
                break;
            
            case "HostChanged":
                // Update lobby owner
                var hostPayload = evt.GetPayload<HostChangedPayload>();
                if (hostPayload != null && CurrentLobbyState != null)
                {
                    CurrentLobbyState.OwnerId = hostPayload.NewHostId;
                    // Update IsOwner flag on all players
                    foreach (var player in CurrentLobbyState.Players)
                    {
                        player.IsOwner = player.ClientUuid == hostPayload.NewHostId;
                    }
                }
                break;
            
            case "GameStarting":
                // Game is entering loading phase - update lobby state if available
                if (CurrentLobbyState != null)
                {
                    CurrentLobbyState.Phase = GamePhase.Loading;
                }
                break;
                
            case "MapDataReady":
                // Map data received - store it and send confirmation
                var mapDataPayload = evt.GetPayload<MapDataReadyPayload>();
                if (mapDataPayload != null && CurrentLobbyState != null)
                {
                    CurrentLobbyState.MapData = mapDataPayload.MapData;
                    CurrentLobbyState.MapDataReady = true;
                }
                // Send confirmation to server
                Send(new MapDataReceived());
                break;
            
            case "GameStarted":
                // Game officially started - update phase
                if (CurrentLobbyState != null)
                {
                    CurrentLobbyState.Phase = GamePhase.Playing;
                }
                break;
                
            case "RoleAssigned":
                var rolePayload = evt.GetPayload<RoleAssignedPayload>();
                if (rolePayload != null && rolePayload.ClientUuid == ClientUuid)
                {
                    MyRole = rolePayload.Role;
                    MyTasks.Clear();
                    if (rolePayload.Tasks != null)
                    {
                        MyTasks.AddRange(rolePayload.Tasks);
                    }
                }
                break;
                
            case "PlayerKilled":
                var killPayload = evt.GetPayload<PlayerKilledPayload>();
                if (killPayload != null)
                {
                    Bodies.Add(new Body
                    {
                        BodyId = killPayload.BodyId,
                        VictimId = killPayload.VictimId,
                        Location = killPayload.Location
                    });
                }
                break;
                
            case "MeetingStarted":
                if (CurrentLobbyState != null)
                {
                    CurrentLobbyState.Phase = GamePhase.Meeting;
                }
                break;
                
            case "VotingClosed":
                Bodies.Clear(); // Bodies zmizí po meetingu
                if (CurrentLobbyState != null)
                {
                    CurrentLobbyState.Phase = GamePhase.Playing;
                }
                break;
                
            case "GameEnded":
                if (CurrentLobbyState != null)
                {
                    CurrentLobbyState.Phase = GamePhase.Ended;
                }
                break;
        }
    }
    
    #endregion
    
    #region Game Actions
    
    public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500, GameSettingsOverrides? settings = null)
    {
        // DisplayName is sent on every CreateLobby/JoinLobby so the server
        // picks up the live nickname (typed into the input field after the
        // ClientHello handshake fired). Without this the server uses the
        // ClientHello-time name, which is the GameManager prefab default
        // for any user who immediately created/joined a lobby.
        Send(new CreateLobby
        {
            PlayAreaCenter = center,
            PlayAreaRadius = playAreaRadius,
            ImpostorCount = impostorCount,
            TaskCount = taskCount,
            Password = password,
            Settings = settings,
            DisplayName = DisplayName
        });
    }

    public void JoinLobby(string joinCode, string? password = null)
    {
        Send(new JoinLobby
        {
            JoinCode = joinCode.ToUpperInvariant(),
            Password = password,
            DisplayName = DisplayName
        });
    }
    
    public void LeaveLobby()
    {
        Send(new LeaveLobby());
        LobbyId = null;
        JoinCode = null;
        CurrentLobbyState = null;
    }
    
    public void StartGame()
    {
        Send(new StartGame());
    }
    
    public void ReturnToLobby()
    {
        Send(new ReturnToLobby());
    }
    
    public void UpdatePosition(Position position)
    {
        MyPosition = position;
        Send(new UpdatePosition { Position = position });
    }
    
    public void Kill(string targetUuid)
    {
        Send(new KillAttempt { TargetClientUuid = targetUuid });
    }
    
    public void ReportBody(string bodyId)
    {
        Send(new ReportBody { BodyId = bodyId });
    }
    
    public void CallEmergencyMeeting()
    {
        Send(new CallEmergencyMeeting());
    }
    
    public void Vote(string? targetUuid)
    {
        Send(new CastVote { TargetClientUuid = targetUuid });
    }
    
    /// <summary>
    /// Pokus o dokončení tasku. Server ověří že hráč je na správné pozici.
    /// </summary>
    public void CompleteTask(string taskId)
    {
        Send(new TaskComplete { TaskId = taskId });
    }
    
    public void SendPing()
    {
        Send(new Ping { ClientTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
    }
    
    public void Reconnect(string lobbyId)
    {
        Send(new Reconnect { LobbyId = lobbyId, LastEventId = LastEventId });
    }
    
    #endregion
    
    #region Helpers
    
    public Body? FindNearbyBody(double maxDistance)
    {
        foreach (var body in Bodies)
        {
            if (MyPosition.DistanceTo(body.Location) <= maxDistance)
            {
                return body;
            }
        }
        return null;
    }
    
    public string? FindNearbyPlayer(double maxDistance, bool aliveOnly = true)
    {
        foreach (var (uuid, info) in PlayerPositions)
        {
            if (uuid == ClientUuid) continue;
            if (aliveOnly && info.State != PlayerState.Alive) continue;
            
            if (MyPosition.DistanceTo(info.Position) <= maxDistance)
            {
                return uuid;
            }
        }
        return null;
    }
    
    public GameTask? FindNearbyTask(double maxDistance)
    {
        foreach (var task in MyTasks)
        {
            if (MyPosition.DistanceTo(task.Location) <= maxDistance)
            {
                return task;
            }
        }
        return null;
    }
    
    // Volat z Unity Update() pro zpracování callbacků
    public void Update()
    {
        Dispatcher.ProcessPendingActions();
    }
    
    #endregion
    
    public void Dispose()
    {
        Disconnect("Disposed");
        _encryption?.Dispose();
    }
}
}
