using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using IStalk.Data;

namespace IStalk.Network
{
    public class RelayClient : MonoBehaviour
    {
        public static RelayClient Instance { get; private set; }

        public event Action OnConnected;
        public event Action OnDisconnected;
        public event Action<string> OnRoomCreated;
        public event Action<string> OnRoomJoined;
        public event Action<int> OnPlayerJoined;
        public event Action<int> OnPlayerLeft;
        public event Action<string> OnError;
        public event Action<byte[]> OnGamePacket;
        public event Action OnGameStart;

        private ClientWebSocket _ws;
        private CancellationTokenSource _cts;
        private bool _connected;
        private readonly Queue<Action> _mainThreadQueue = new();
        private readonly byte[] _receiveBuffer = new byte[4096];

        public bool IsConnected => _connected;
        public string CurrentRoomCode { get; private set; }
        public int LocalPeerId { get; private set; }

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        private void Update()
        {
            lock (_mainThreadQueue)
            {
                while (_mainThreadQueue.Count > 0)
                    _mainThreadQueue.Dequeue()?.Invoke();
            }
        }

        private void OnDestroy()
        {
            Disconnect();
        }

        public async void Connect()
        {
            if (_connected) return;

            string url = GlobalRules_SO.Instance.GetRelayUrl();
            _ws = new ClientWebSocket();
            _cts = new CancellationTokenSource();

            try
            {
                await _ws.ConnectAsync(new Uri(url), _cts.Token);
                _connected = true;
                Enqueue(() => OnConnected?.Invoke());
                _ = ReceiveLoop();
            }
            catch (Exception ex)
            {
                Enqueue(() => OnError?.Invoke(ex.Message));
            }
        }

        public void Disconnect()
        {
            if (_ws == null) return;
            _cts?.Cancel();

            try
            {
                if (_ws.State == WebSocketState.Open)
                    _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            }
            catch { }

            _ws.Dispose();
            _ws = null;
            _connected = false;
            CurrentRoomCode = null;
        }

        public void CreateRoom(string playerName)
        {
            SendJson($"{{\"type\":\"create\",\"playerName\":\"{playerName}\"}}");
        }

        public void JoinRoom(string roomCode, string playerName)
        {
            SendJson($"{{\"type\":\"join\",\"roomCode\":\"{roomCode}\",\"playerName\":\"{playerName}\"}}");
        }

        public void StartGame(int mapSeed)
        {
            SendJson($"{{\"type\":\"start\",\"mapSeed\":{mapSeed}}}");
        }

        public void SendGamePacket(byte[] data)
        {
            if (!_connected || _ws == null || _ws.State != WebSocketState.Open) return;

            byte[] packet = new byte[data.Length + 1];
            packet[0] = 0x01;
            Buffer.BlockCopy(data, 0, packet, 1, data.Length);

            _ = _ws.SendAsync(new ArraySegment<byte>(packet), WebSocketMessageType.Binary, true, _cts.Token);
        }

        private async Task ReceiveLoop()
        {
            try
            {
                while (_ws != null && _ws.State == WebSocketState.Open && !_cts.IsCancellationRequested)
                {
                    var result = await _ws.ReceiveAsync(new ArraySegment<byte>(_receiveBuffer), _cts.Token);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        Enqueue(() =>
                        {
                            _connected = false;
                            CurrentRoomCode = null;
                            OnDisconnected?.Invoke();
                        });
                        return;
                    }

                    byte[] data = new byte[result.Count];
                    Buffer.BlockCopy(_receiveBuffer, 0, data, 0, result.Count);

                    if (result.MessageType == WebSocketMessageType.Binary && data.Length > 0 && data[0] == 0x01)
                    {
                        byte[] gameData = new byte[data.Length - 1];
                        Buffer.BlockCopy(data, 1, gameData, 0, gameData.Length);
                        Enqueue(() => OnGamePacket?.Invoke(gameData));
                    }
                    else
                    {
                        string json = Encoding.UTF8.GetString(data, 0, data.Length);
                        HandleJsonMessage(json);
                    }
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                Enqueue(() => OnError?.Invoke(ex.Message));
            }
            finally
            {
                Enqueue(() =>
                {
                    _connected = false;
                    CurrentRoomCode = null;
                    OnDisconnected?.Invoke();
                });
            }
        }

        private void HandleJsonMessage(string json)
        {
            var msg = JsonUtility.FromJson<RelayMessage>(json);
            if (msg == null) return;

            Enqueue(() =>
            {
                switch (msg.type)
                {
                    case "created":
                        CurrentRoomCode = msg.roomCode;
                        LocalPeerId = msg.peerId;
                        GameStartData.IsHost = true;
                        OnRoomCreated?.Invoke(msg.roomCode);
                        break;
                    case "joined":
                        CurrentRoomCode = msg.roomCode;
                        LocalPeerId = msg.peerId;
                        OnRoomJoined?.Invoke(msg.roomCode);
                        break;
                    case "playerJoined":
                        OnPlayerJoined?.Invoke(msg.peerId);
                        break;
                    case "playerLeft":
                        OnPlayerLeft?.Invoke(msg.peerId);
                        break;
                    case "gameStart":
                        GameStartData.MapSeed = msg.mapSeed;
                        OnGameStart?.Invoke();
                        break;
                    case "error":
                        OnError?.Invoke(msg.message);
                        break;
                }
            });
        }

        private void SendJson(string json)
        {
            if (!_connected || _ws == null || _ws.State != WebSocketState.Open) return;
            byte[] bytes = Encoding.UTF8.GetBytes(json);
            _ = _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cts.Token);
        }

        private void Enqueue(Action action)
        {
            lock (_mainThreadQueue)
                _mainThreadQueue.Enqueue(action);
        }
    }

    [Serializable]
    public class RelayMessage
    {
        public string type;
        public string roomCode;
        public int peerId;
        public int mapSeed;
        public string message;
    }
}
