using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Matchmaker;
using Unity.Services.Matchmaker.Models;
using StatusOptions = Unity.Services.Matchmaker.Models.MultiplayAssignment.StatusOptions;
using UnityEngine;
using TMPro;
using UnityEngine.Events;
using System.Threading;
using Cysharp.Threading.Tasks;
using UDB;
#if UNITY_EDITOR
using ParrelSync;
#endif

public class QuickMatchmakerServiceModule : MonoBehaviour
{
    public TextMeshProUGUI statusText;
    public UnityEvent OnDidJoinMatch;
    public MatchmakingService matchmakingService;

    private CancellationTokenSource cancelToken;

    public void JoinQuickMatch()
    {
        cancelToken = new CancellationTokenSource();
        statusText.text = "finding quick match...";
        StartClient();
    }

    public void Cancel()
    {
        Debug.Log("Cancel ticket");
        cancelToken?.Cancel();
    }

    public static string _ticketId {
        get { 
            return PlayerPrefs.GetString("Match-Macking-Last-Ticket");
        }
        set {
            PlayerPrefs.SetString("Match-Macking-Last-Ticket", value);
        }
    }

    /// <summary>
    /// Helped function to get the ID of the player connected to Unity Services.
    /// </summary>
    private string PlayerID()
    {
        return NetworkSession.guid.ToString();
    }

    /// <summary>
    /// Starts the matchmaking process.
    /// </summary>
    private async void StartClient()
    {
        Debug.Log("StartClient");
        if (gameObject.activeInHierarchy == false) {
            return;
        }
        if (cancelToken.IsCancellationRequested) {
            return;
        }

        if (NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer) {
            Debug.Log("Shutting Down Previous Match...");
            NetworkManager.Singleton.Shutdown();
        }

        while (!cancelToken.IsCancellationRequested) {
            Debug.Log("Join Quick Match");
            if (Authentication.IsLoggedIn() == false) {
                Debug.Log("Will log in");
                statusText.text = "Loging in to service...";
                await Authentication.Login();
                statusText.text = "Logged into service...";
                Debug.Log("Did log in");
            }
            if (Authentication.IsLoggedIn() == false) {
                statusText.text = "Login attemp failed, trying again...";
                Debug.Log("Retry in a few seconds");
                await UniTask.Delay(TimeSpan.FromMilliseconds(2000), ignoreTimeScale: false);
                continue;
            }
            break;
        }
        Debug.Log("create or poll ticket");
        if (_ticketId.IsNullOrEmpty()) {
            CreateATicket();
        } else {
            PollTicketStatus();
        }
    }

    /// <summary>
    /// Creates a matchmaking ticket with the passed in player properties.
    /// </summary>
    private async void CreateATicket()
    {
        if (cancelToken.IsCancellationRequested) {
            return;
        }
        statusText.text = "Creating Ticket...";
        var options = new CreateTicketOptions("default-queue");

        var players = new List<Player>
        {
            new Player(
                PlayerID()
            )
        };

        var ticketResponse = await MatchmakerService.Instance.CreateTicketAsync(players, options);
        _ticketId = ticketResponse.Id;
        Debug.Log($"Ticket ID: {_ticketId}");
        PollTicketStatus();
    }

    /// <summary>
    /// Loops until the ticket status is found, failed, or timed out. If the ticket is found, calls TicketAssigned.
    /// </summary>
    /// <exception cref="InvalidOperationException"></exception>
    private async void PollTicketStatus()
    {
        if (cancelToken.IsCancellationRequested) {
            return;
        }
        MultiplayAssignment multiplayAssignment = null;
        bool gotAssignment = false;
        do {
            Debug.Log($"PollTicketStatus in a sec for ticket {_ticketId}");
            await UniTask.Delay(TimeSpan.FromMilliseconds(1100), ignoreTimeScale: true);
            Debug.Log($"PollTicketStatus now for ticket {_ticketId}");
            TicketStatusResponse ticketStatus = null;
            try {
                ticketStatus = await MatchmakerService.Instance.GetTicketAsync(_ticketId);
            }
            catch (Exception ex) {
                Debug.LogWarning($"Something went wrong trying to GetTicketAsync:\n{ex}");
                Debug.Log(ex.Message);
                if (ex.Message.Contains("404 Not Found")) {
                    CreateATicket();
                    return;
                }
                continue;
            }
            if (cancelToken.IsCancellationRequested) {
                return;
            }
            Debug.Log($"Ticket got ticketStatus {ticketStatus.Type.ToString()}");
            if (ticketStatus == null) {
                Debug.Log($"Ticket ID with {_ticketId} is null");
                CreateATicket();
                return;
            }
            if (ticketStatus.Type == typeof(MultiplayAssignment)) {
                multiplayAssignment = ticketStatus.Value as MultiplayAssignment;
            }

            switch (multiplayAssignment.Status) {
                case StatusOptions.Found:
                    statusText.text = "Ticket found...";
                    gotAssignment = true;
                    TicketAssigned(multiplayAssignment);
                    Debug.Log($"Ticket found");
                    break;
                case StatusOptions.InProgress:
                    statusText.text = "Looking Really Hard...";
                    Debug.Log($"Ticket InProgress");
                    break;
                case StatusOptions.Failed:
                    gotAssignment = true;
                    statusText.text = $"Failed to get ticket status. Error: {multiplayAssignment.Message}";
                    Debug.LogError($"Failed to get ticket status. Error: {multiplayAssignment.Message}");
                    _ticketId = null;
                    await UniTask.Delay(TimeSpan.FromMilliseconds(2000), ignoreTimeScale: true);
                    StartClient();
                    return;
                case StatusOptions.Timeout:
                    gotAssignment = true;
                    statusText.text = $"Failed to get ticket status. Ticket timed out.";
                    Debug.LogError("Failed to get ticket status. Ticket timed out.");
                    _ticketId = null;
                    await UniTask.Delay(TimeSpan.FromMilliseconds(2000), ignoreTimeScale: true);
                    StartClient();
                    return;
                default:
                    throw new InvalidOperationException();
            }
        } while (!gotAssignment && !cancelToken.IsCancellationRequested);
    }

    /// <summary>
    /// Called when the ticket is found. Sets the connection data and starts the client.
    /// </summary>
    /// <param name="assignment">The server assignment that was previously found.</param>
    private async void TicketAssigned(MultiplayAssignment assignment)
    {
        if (cancelToken.IsCancellationRequested) {
            return;
        }
        statusText.text = "Ticket Assigned!";
        Debug.Log($"Ticket Assigned: {assignment.Ip}:{assignment.Port}");
        NetworkManager.Singleton.NetworkConfig.ConnectionApproval = true;
#if DIRECTSERVER
        NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(assignment.Ip, (ushort)assignment.Port);

        int attemptCount = 0;
        while (NetworkManager.Singleton.IsConnectedClient == false) {
            await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
            OnDidJoinMatch?.Invoke();
            await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
            NetworkManager.Singleton.StartClient();
            attemptCount++;
            if (attemptCount > 5) {
                // this might mean that everybody else left the server and now we need to find a new server
                CreateATicket();
                return;
            }
        }
        NetworkSceneDirector.instance.OnClientStarted(true);
        Debug.Log("Did join match");
        statusText.text = "Did join Match...";

        //for some fucking reason we sometimes get stuck without the starting scene from the server. A reconnect fixes this.
        await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
        int loadedScenes = 0;
        for (int n = 0; n < SceneManager.sceneCount; ++n) {
            Scene scene = SceneManager.GetSceneAt(n);
            if (scene.name != UDB.Constants.coreScene && scene.name != "NetworkCore") {
                loadedScenes++;
            }
        }
        if (loadedScenes == 0) {
            NetworkManager.Singleton.Shutdown();
            await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
            NetworkManager.Singleton.StartClient();
        }
#else
        int attemptCount = 0;
        Unity.Services.Lobbies.Models.Lobby lobby = null;
        while (lobby == null) {
            statusText.text = "Trying to find Lobby";
            lobby = await matchmakingService.FindLobbyWithMatchID(assignment.MatchId);
            if (lobby == null) {
                Debug.Log($"Trying to find lobby again in 1 sec");
                await UniTask.Delay(TimeSpan.FromMilliseconds(1000), ignoreTimeScale: true);
            }
            attemptCount++;
            if (attemptCount > 10) {
                // this might mean that everybody else left the server and now we need to find a new server
                Debug.Log($"Failed to find Lobby");
                CreateATicket();
                return;
            }
        }
        statusText.text = "Found Match";
        bool joinSuccess = false;
        while (joinSuccess == false) {
            attemptCount++;
            if (attemptCount > 10) {
                // this might mean that everybody else left the server and now we need to find a new server
                Debug.Log($"Failed to join Lobby");
                CreateATicket();
                return;
            }
            statusText.text = "Trying to join Lobby";
            joinSuccess = await matchmakingService.JoinLobbyWithAllocation(lobby.Id);
            if (joinSuccess == false) {
                Debug.Log($"Trying to join lobby again in 1 sec");
                await UniTask.Delay(TimeSpan.FromMilliseconds(1000), ignoreTimeScale: true);
            }
        }
        statusText.text = "Joined Lobby";
        OnDidJoinMatch?.Invoke();

        attemptCount = 0;
        while (NetworkManager.Singleton.IsConnectedClient == false) {
            if (NetworkManager.Singleton.StartClient()) {
                NetworkSceneDirector.instance.OnClientStarted(true);
            }
            attemptCount++;
            if (attemptCount > 10) {
                // this might mean that everybody else left the server and now we need to find a new server
                Debug.Log($"Failed to connect to client");
                CreateATicket();
                return;
            }
            if (NetworkManager.Singleton.IsConnectedClient == false) {
                Debug.Log($"Trying to connect to client in 1 sec");
                await UniTask.Delay(TimeSpan.FromMilliseconds(5000), ignoreTimeScale: true);
            }
        }
        Debug.Log("Did join match");
        statusText.text = "Did join Match...";
#endif
    }
}
