using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using UDB;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport.Relay;
using Unity.Services.Authentication;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
using Cysharp.Threading.Tasks;

[CreateAssetMenu(menuName = "UDB/MatchmakingService")]
public class MatchmakingService : ScriptableObject {
    public struct LobbyData
    {
        public string Name;
        public int MaxPlayers;
        public int Difficulty;
        public int Type;
        public string MatchId;
    }

    private const int HeartbeatInterval = 15;
    private const int LobbyRefreshRate = 2; // Rate limits at 2

    private UnityTransport _transport;

    private Lobby _currentLobby;
    private Guid lastHostSessionID;
    private CancellationTokenSource _heartbeatSource, _updateLobbySource;

    [System.NonSerialized]
    private string _buildInfo = "";
    public string GetBuildInfo()
    {
        if (_buildInfo.Length == 0) {
            _buildInfo = "development";
            var releaseType = Resources.Load<TextAsset>("BuildType");
            if (releaseType != null) {
                _buildInfo = releaseType.text;
            }
            var gitHash = Resources.Load<TextAsset>("GitHash");
            if (gitHash != null) {
                _buildInfo += "-" + gitHash.text;
            }
        }
        Debug.Log("Using build info: " + _buildInfo);
        return _buildInfo;
    }

    private UnityTransport Transport {
        get => _transport != null ? _transport : _transport = UnityEngine.Object.FindFirstObjectByType<UnityTransport>();
        set => _transport = value;
    }

    public event Action<Lobby> CurrentLobbyRefreshed;

    public event Action IntentionallyLeavingLobby;

    public void ResetStatics() {
        if (Transport != null) {
            Transport.Shutdown();
            Transport = null;
        }

        lastHostSessionID = Guid.Empty;
        _currentLobby = null;
    }

    public bool InLobby()
    {
        return _currentLobby != null;
    }

    public Guid LastHostSessionID()
    {
        return lastHostSessionID;
    }

    // Obviously you'd want to add customization to the query, but this
    // will suffice for this simple demo
    public async Task<List<Lobby>> GatherLobbies() {
        var options = new QueryLobbiesOptions {
            Count = 15,

            Filters = new List<QueryFilter> {
                new(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT),
                new(QueryFilter.FieldOptions.IsLocked, "0", QueryFilter.OpOptions.EQ),
                new(QueryFilter.FieldOptions.S2, GetBuildInfo(), QueryFilter.OpOptions.EQ),
                new(QueryFilter.FieldOptions.S3, "", QueryFilter.OpOptions.EQ)
            }
        };

        var allLobbies = await Lobbies.Instance.QueryLobbiesAsync(options);
        return allLobbies.Results;
    }

    public async Task<Lobby> FindLobbyWithSessionID(Guid id)
    {
        var options = new QueryLobbiesOptions {
            Count = 1,

            Filters = new List<QueryFilter> {
                new(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT),
                new(QueryFilter.FieldOptions.IsLocked, "0", QueryFilter.OpOptions.EQ),
                new(QueryFilter.FieldOptions.S1, id.ToString(), QueryFilter.OpOptions.EQ),
                new(QueryFilter.FieldOptions.S2, GetBuildInfo(), QueryFilter.OpOptions.EQ)
            }
        };

        var allLobbies = await Lobbies.Instance.QueryLobbiesAsync(options);
        if (allLobbies.Results.Count == 0) {
            return null;
        }
        return allLobbies.Results[0];
    }

    public async Task<Lobby> FindLobbyWithMatchID(String matchID)
    {
        var options = new QueryLobbiesOptions {
            Count = 1,

            Filters = new List<QueryFilter> {
                new(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT),
                new(QueryFilter.FieldOptions.IsLocked, "0", QueryFilter.OpOptions.EQ),
                //Re enable once it is important to make sure clinets and servers run the same exact build
                //new(QueryFilter.FieldOptions.S2, GetBuildInfo(), QueryFilter.OpOptions.EQ),
                new(QueryFilter.FieldOptions.S3, matchID, QueryFilter.OpOptions.EQ)
            }
        };

        var allLobbies = await Lobbies.Instance.QueryLobbiesAsync(options);
        if (allLobbies.Results.Count == 0) {
            return null;
        }
        return allLobbies.Results[0];
    }

    public async Task CreateLobbyWithAllocation(LobbyData data) {
        if (_currentLobby != null) {
            await AsyncLeaveLobby();
        }

        // Create a relay allocation and generate a join code to share with the lobby
        var a = await RelayService.Instance.CreateAllocationAsync(data.MaxPlayers);
        Transport.SetRelayServerData(new RelayServerData(a, "dtls"));
        var joinCode = await RelayService.Instance.GetJoinCodeAsync(a.AllocationId);

        // Create a lobby, adding the relay join code to the lobby data
        var options = new CreateLobbyOptions {
            Data = new Dictionary<string, DataObject> {
                { Constants.JoinKey, new DataObject(DataObject.VisibilityOptions.Member, joinCode) },
                { Constants.SessionIDKey, new DataObject(DataObject.VisibilityOptions.Member, NetworkSession.guid.ToString(), DataObject.IndexOptions.S1) },
                { Constants.BuildInfoKey, new DataObject(DataObject.VisibilityOptions.Public, GetBuildInfo(), DataObject.IndexOptions.S2) },
                { Constants.MatchIDKey, new DataObject(DataObject.VisibilityOptions.Public, data.MatchId, DataObject.IndexOptions.S3) },
                { Constants.GameTypeKey, new DataObject(DataObject.VisibilityOptions.Public, data.Type.ToString(), DataObject.IndexOptions.N1) },
                { Constants.DifficultyKey, new DataObject(DataObject.VisibilityOptions.Public, data.Difficulty.ToString(), DataObject.IndexOptions.N2) },
            }
        };

        _currentLobby = await Lobbies.Instance.CreateLobbyAsync(data.Name, data.MaxPlayers, options);

        Heartbeat();
        PeriodicallyRefreshLobby();
    }

    public async Task LockLobby() {
        try {
            await Lobbies.Instance.UpdateLobbyAsync(_currentLobby.Id, new UpdateLobbyOptions { IsLocked = true });
        }
        catch (Exception e) {
            Debug.Log($"Failed closing lobby: {e}");
        }
    }

    private async void Heartbeat() {
        _heartbeatSource = new CancellationTokenSource();
        while (!_heartbeatSource.IsCancellationRequested && _currentLobby != null) {
            await Lobbies.Instance.SendHeartbeatPingAsync(_currentLobby.Id);
            await UniTask.Delay(HeartbeatInterval * 1000);
        }
    }

    public async Task<bool> JoinLobbyWithAllocation(string lobbyId) {
        if (_currentLobby != null) {
            await AsyncLeaveLobby();
        }

        try {
            _currentLobby = await Lobbies.Instance.JoinLobbyByIdAsync(lobbyId);
        }
        catch (LobbyServiceException e) {
            Debug.Log(e);
            return false;
        }
        lastHostSessionID = new Guid(_currentLobby.Data[Constants.SessionIDKey].Value);
        JoinAllocation a = await RelayService.Instance.JoinAllocationAsync(_currentLobby.Data[Constants.JoinKey].Value);

#if UNITY_WEBGL
        try {
            Transport.SetRelayServerData(new RelayServerData(a, "wss"));
            Transport.UseWebSockets = true;
        }
        catch (SocketException e) {
            Debug.Log(e);
            return false;
        }
#else
        Transport.SetRelayServerData(new RelayServerData(a, "dtls"));
#endif

        PeriodicallyRefreshLobby();
        return true;
    }

    private async void PeriodicallyRefreshLobby() {
        _updateLobbySource = new CancellationTokenSource();
        await UniTask.Delay(LobbyRefreshRate * 1000);
        while (!_updateLobbySource.IsCancellationRequested && _currentLobby != null) {
            _currentLobby = await Lobbies.Instance.GetLobbyAsync(_currentLobby.Id);
            CurrentLobbyRefreshed?.Invoke(_currentLobby);
            await UniTask.Delay(LobbyRefreshRate * 1000);
        }
    }

    public void LeaveLobby()
    {
        IntentionallyLeavingLobby?.Invoke();
        if (NetworkManager.Singleton != null) {
            NetworkManager.Singleton.Shutdown();
        }
        _ = AsyncLeaveLobby();
    }

    public async Task AsyncLeaveLobby() {
        _heartbeatSource?.Cancel();
        _updateLobbySource?.Cancel();

        if (_currentLobby != null)
            try {
                if (_currentLobby.HostId == AuthenticationService.Instance.PlayerId) await Lobbies.Instance.DeleteLobbyAsync(_currentLobby.Id);
                else await Lobbies.Instance.RemovePlayerAsync(_currentLobby.Id, AuthenticationService.Instance.PlayerId);
                _currentLobby = null;
            }
            catch (Exception e) {
                Debug.Log(e);
            }
    }
}