using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Link.Natayark;
using PCL.Core.Link.Scaffolding;
using PCL.Core.Link.Scaffolding.Client.Models;
using PCL.Core.Link.Scaffolding.EasyTier;
using PCL.Core.Logging;
using PCL.Core.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using PCL.Core.App.IoC;
using PCL.Core.UI;
using PCL.Core.Link.McPing;

namespace PCL.Core.Link.Lobby;

/// <summary>
/// Lobby server. For auto-management
/// </summary>
[LifecycleService(LifecycleState.Loaded)]
public class LobbyService() : GeneralService("lobby", "LobbyService")
{
    private static readonly LobbyController _LobbyController = new();
    private static CancellationTokenSource _lobbyCts = new();

    private static Task? _discoveringTask;
    private static CancellationTokenSource _discoveringCts = new();

    private static readonly Timer _ServerGameWatcher =
        new(_CheckGameState, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

    private static bool _isGameWatcherRunnable = false;
    private static int _isLeaving;

    /// <summary>
    /// Current lobby state.
    /// </summary>
    public static LobbyState CurrentState { get; private set; } = LobbyState.Idle;


    /// <summary>
    /// Founded local Minecraft worlds.
    /// </summary>
    public static ObservableCollection<FoundWorld> DiscoveredWorlds { get; } = [];

    /// <summary>
    /// Current players in current lobby.
    /// </summary>
    public static ObservableCollection<PlayerProfile> Players { get; private set; } = [];

    /// <summary>
    /// Demonstrate whether the current user is the host of the lobby.
    /// </summary>
    public static bool IsHost => _LobbyController.IsHost;

    /// <summary>
    /// Current lobby full code.
    /// </summary>
    public static string? CurrentLobbyCode { get; private set; }

    /// <summary>
    /// Current lobby username.
    /// </summary>
    public static string? CurrentUserName { get; private set; }

    #region UI Events

    /// <summary>
    /// Invoked when lobby state changed. (first arg is the old state; second arg is the new state.)
    /// </summary>
    public static event Action<LobbyState, LobbyState>? StateChanged;

    /// <summary>
    /// Invoked when need to download EasyTier core files.
    /// </summary>
    public static event Action? OnNeedDownloadEasyTier;

    /// <summary>
    /// Invoked when user stop the game in server mode.
    /// </summary>
    public static event Action? OnUserStopGame;

    /// <summary>
    /// Invoked when client ping happened.
    /// </summary>
    public static event Action<long>? OnClientPing;

    /// <summary>
    /// Invoked when server shut down.
    /// </summary>
    public static event Action? OnServerShutDown;


    /// <summary>
    /// Invoked when server started successfully.
    /// </summary>
    public static event Action? OnServerStarted;

    public static event Action<Exception>? OnServerException;

    #endregion

    /// <inheritdoc />
    public override void Stop()
    {
        _lobbyCts.Cancel();
        _LobbyController.CloseAsync().GetAwaiter().GetResult();
        _ServerGameWatcher.Dispose();
        _lobbyCts.Dispose();

        _discoveringCts.Cancel();
        if (_discoveringTask is not null)
        {
            Task.WhenAll(_discoveringTask);
            _discoveringTask.Dispose();
        }

        _discoveringCts.Dispose();
    }

    private static bool _IsEasyTierCoreFileNotExist() =>
        !File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-core.exe")) &&
        !File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "Packet.dll")) &&
        !File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-cli.exe"));


    public static async Task InitializeAsync()
    {
        if (CurrentState is not LobbyState.Idle && CurrentState is not LobbyState.Error)
        {
            return;
        }

        _SetState(LobbyState.Initializing);
        try
        {
            if (_IsEasyTierCoreFileNotExist())
            {
                LogWrapper.Info("LobbyService", "EasyTier not found, starting download.");
                OnNeedDownloadEasyTier?.Invoke();
            }
            else
            {
                LogWrapper.Info("LobbyService", "EasyTier files check completed.");
            }

            // refresh naid token
            var naidRefreshToken = States.Link.NaidRefreshToken;
            if (!string.IsNullOrWhiteSpace(naidRefreshToken))
            {
                var expTime = States.Link.NaidRefreshExpireTime;
                if (!string.IsNullOrWhiteSpace(expTime) &&
                    Convert.ToDateTime(expTime).CompareTo(DateTime.Now) < 0)
                {
                    States.Link.NaidRefreshToken = string.Empty;
                    HintWrapper.Show(Lang.Text("Tools.GameLink.Natayark.TokenExpired"), HintTheme.Error);
                }
                else
                {
                    await NatayarkProfileManager.GetNaidDataAsync(naidRefreshToken, true).ConfigureAwait(false);
                }
            }

            _SetState(LobbyState.Initialized);
            LogWrapper.Info("LobbyService", "Lobby service initialized successfully.");

            _ = DiscoverWorldAsync();
        }
        catch (Exception ex)
        {
            LogWrapper.Error(ex, "LobbyService", "Lobby service initialization failed.");
            HintWrapper.Show(Lang.Text("Link.Lobby.InitFailed"), HintTheme.Error);
            _SetState(LobbyState.Error);
        }
    }

    /// <summary>
    /// Discover minecraft shared world.
    /// </summary>
    public static async Task DiscoverWorldAsync()
    {
        if (_discoveringCts.IsCancellationRequested)
        {
            return;
        }

        if (CurrentState is not LobbyState.Initialized && CurrentState is not LobbyState.Idle)
        {
            return;
        }

        _SetState(LobbyState.Discovering);
        await _RunInUiAsync(() => DiscoveredWorlds.Clear()).ConfigureAwait(false);

        _discoveringTask = Task.Run(async () =>
        {
            var recordedPorts = new ConcurrentSet<int>();
            using var listener = new BroadcastListener();

            var handler = new Action<BroadcastRecord, IPEndPoint>((info, _) => Task.Run(async () =>
            {
                if (!recordedPorts.TryAdd(info.Address.Port)) return;

                using var pinger = McPingServiceFactory.CreateService(new IPEndPoint(IPAddress.Loopback, info.Address.Port));
                using var cts = new CancellationTokenSource(2000);

                try
                {
                    var pingRes = await pinger.PingAsync(cts.Token).ConfigureAwait(false);

                    if (pingRes is null)
                    {
                        throw new ArgumentNullException(nameof(pingRes), "Failed to ping minecraft entity.");
                    }

                    var worldName = Lang.Text("Link.Lobby.WorldNameFormat",
                        pingRes.Description,
                        pingRes.Version.Name,
                        info.Address.Port);
                    await _RunInUiAsync(() => DiscoveredWorlds.Add(new FoundWorld(worldName, info.Address.Port)))
                        .ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    LogWrapper.Error(ex, "LobbyService", $"Pinging port {info.Address.Port} failed.");
                }
            }));

            listener.OnReceive += handler;
            listener.Start();
            await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
            listener.OnReceive -= handler;
        }, _discoveringCts.Token);

        _SetState(LobbyState.Initialized);
    }

    private static bool _NotHaveNaid() =>
        LobbyInfoProvider.RequiresLogin &&
        string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.AccessToken);

    /// <summary>
    /// Create a new lobby.
    /// </summary>
    /// <param name="port">Minecraft share port.</param>
    /// <param name="username">Player name.</param>
    public static async Task<bool> CreateLobbyAsync(int port, string username)
    {
        if (_NotHaveNaid())
        {
            HintWrapper.Show(Lang.Text("Link.Lobby.LoginRequired"), HintTheme.Error);
            return false;
        }

        await _discoveringCts.CancelAsync().ConfigureAwait(false);

        _SetState(LobbyState.Creating);
        try
        {
            CurrentUserName = username;

            var serverEntity = await _LobbyController.LaunchServerAsync(username, port).ConfigureAwait(false);
            if (serverEntity is null)
            {
                HintWrapper.Show(Lang.Text("Link.Lobby.CreateRoomFailed"), HintTheme.Error);
                return false;
            }

            CurrentLobbyCode = serverEntity.EasyTier.Lobby.FullCode;

            serverEntity.Server.ServerStopped += () => OnServerShutDown?.Invoke();
            serverEntity.Server.PlayerProfilePing += _ServerOnPlayerPing;
            serverEntity.Server.ServerStarted += _ServerOnServerStarted;
            serverEntity.Server.ServerException += _ServerOnServerException;
            //serverEntity.EasyTier.EasyTierProcessExisted += () =>
            //{
            //    OnHint?.Invoke("EasyTierCore异常退出", CoreHintType.Critical);
            //    OnServerShutDown?.Invoke();
            //}; this code will be invoked when EasyTier process successfully exited, not in failed state.

            serverEntity.Server.Start();

            _SetState(LobbyState.Connected);
            _isGameWatcherRunnable = true;
        }
        catch (Exception ex)
        {
            LogWrapper.Error(ex, "LobbyService", "Failed to create lobby.");
            HintWrapper.Show(Lang.Text("Link.Lobby.CreateLobbyFailed"), HintTheme.Error);
            await LeaveLobbyAsync().ConfigureAwait(false);

            return false;
        }

        return true;
    }

    private static void _ServerOnServerException(Exception? ex)
    {
        if (ex is null)
        {
            return;
        }

        OnServerException?.Invoke(ex);
    }

    private static void _ServerOnServerStarted(IReadOnlyList<PlayerProfile> profiles)
    {
        LogWrapper.Debug("LobbyService", "Send server started event.");
        OnServerStarted?.Invoke();
        _ServerOnPlayerPing(profiles);
    }

    private static void _ServerOnPlayerPing(IReadOnlyList<PlayerProfile> players)
    {
        _ = _RunInUiAsync(() =>
        {
            var currentMachineIds = new HashSet<string>(Players.Select(p => p.MachineId));
            var newMachineIds = new HashSet<string>(players.Select(p => p.MachineId));
            
            if (currentMachineIds.SetEquals(newMachineIds))
            {
                LogWrapper.Debug("Player list has not changed");
                return; // nothing was changed
            }

            LogWrapper.Debug("LobbyService", "Player list membership has changed, updating UI.");

            var sortedNewPlayers = PlayerListHandler.Sort(players);

            var idsToRemove = currentMachineIds.Except(newMachineIds).ToList();
            if (idsToRemove.Any())
            {
                var playersToRemove = Players.Where(p => idsToRemove.Contains(p.MachineId)).ToList();
                foreach (var player in playersToRemove)
                {
                    Players.Remove(player);
                }
            }

            var idsToAdd = newMachineIds.Except(currentMachineIds).ToList();
            if (idsToAdd.Any())
            {
                var playersToAdd = sortedNewPlayers.Where(p => idsToAdd.Contains(p.MachineId)).ToList();
                foreach (var player in playersToAdd)
                {
                    Players.Add(player);
                }
            }
        });
    }

    /// <summary>
    /// Join an exist lobby.
    /// </summary>
    /// <param name="lobbyCode">Lobby share code.</param>
    /// <param name="username">Current use name.</param>
    public static async Task<bool> JoinLobbyAsync(string lobbyCode, string username)
    {
        await _discoveringCts.CancelAsync().ConfigureAwait(false);

        _SetState(LobbyState.Joining);

        LogWrapper.Info("LobbyService", $"Try to join lobby {lobbyCode}");

        try
        {
            CurrentUserName = username;
            CurrentLobbyCode = lobbyCode;

            var clientEntity = await _LobbyController.LaunchClientAsync(username, lobbyCode, _lobbyCts.Token).ConfigureAwait(false);

            if (clientEntity is null)
            {
                throw new InvalidOperationException(
                    Lang.Text("Link.Lobby.JoinFailed"));
            }

            clientEntity.Client.Heartbeat += _ClientOnHeartbeat;
            clientEntity.Client.ServerShuttedDown += _ClientOnServerShutDown;

            _SetState(LobbyState.Connected);
        }
        catch (ArgumentException codeEx)
        {
            LogWrapper.Error(codeEx, "LobbyService", $"Failed to join lobby {lobbyCode}.");
            HintWrapper.Show(Lang.Text("Link.Lobby.InvalidCodeFormat"), HintTheme.Error);
            await LeaveLobbyAsync().ConfigureAwait(false);

            return false;
        }
        catch (OperationCanceledException)
        {
            return false;
        }
        catch (Exception ex)
        {
            LogWrapper.Error(ex, "LobbyService", $"Failed to join lobby {lobbyCode}.");
            HintWrapper.Show(ex.Message, HintTheme.Error);
            await LeaveLobbyAsync().ConfigureAwait(false);

            return false;
        }

        return true;
    }

    private static void _ClientOnServerShutDown()
    {
        OnServerShutDown?.Invoke();
    }

    private static void _ClientOnHeartbeat(IReadOnlyList<PlayerProfile> players, long latency)
    {
        _ = _RunInUiAsync(() =>
        {
            var currentMachineIds = new HashSet<string>(Players.Select(p => p.MachineId));
            var newMachineIds = new HashSet<string>(players.Select(p => p.MachineId));

            if (currentMachineIds.SetEquals(newMachineIds))
            {
                return; // nothing was changed
            }

            LogWrapper.Debug("LobbyService", "Player list membership has changed, updating UI.");

            var sortedNewPlayers = PlayerListHandler.Sort(players);

            var idsToRemove = currentMachineIds.Except(newMachineIds).ToList();
            if (idsToRemove.Any())
            {
                var playersToRemove = Players.Where(p => idsToRemove.Contains(p.MachineId)).ToList();
                foreach (var player in playersToRemove)
                {
                    Players.Remove(player);
                }
            }

            var idsToAdd = newMachineIds.Except(currentMachineIds).ToList();
            if (idsToAdd.Any())
            {
                var playersToAdd = sortedNewPlayers.Where(p => idsToAdd.Contains(p.MachineId)).ToList();
                foreach (var player in playersToAdd)
                {
                    Players.Add(player);
                }
            }

            OnClientPing?.Invoke(latency);
        });
    }


    /// <summary>
    /// Leave from lobby.
    /// </summary>
    public static async Task LeaveLobbyAsync()
    {
        if (Interlocked.Exchange(ref _isLeaving, 1) == 1)
            return;

        try
        {
            _SetState(LobbyState.Leaving);

            await _lobbyCts.CancelAsync().ConfigureAwait(false);

            Players.Clear();
            CurrentLobbyCode = null;
            CurrentUserName = null;

            if (_LobbyController.ScfClientEntity?.Client is not null)
            {
                _LobbyController.ScfClientEntity.Client.Heartbeat -= _ClientOnHeartbeat;
            }

            if (_LobbyController.ScfServerEntity?.Server is not null)
            {
                _LobbyController.ScfServerEntity.Server.PlayerProfilePing -= _ServerOnPlayerPing;
                _LobbyController.ScfServerEntity.Server.ServerStarted -= _ServerOnServerStarted;
            }

            await _LobbyController.CloseAsync().ConfigureAwait(false);


            _lobbyCts = new CancellationTokenSource();
            _SetState(LobbyState.Initialized);

            LogWrapper.Info("LobbyService", "Left lobby and cleaned up resources.");

            _isGameWatcherRunnable = false;

            _discoveringCts = new CancellationTokenSource();
            _ = DiscoverWorldAsync();
        }
        catch (Exception ex)
        {
            LogWrapper.Error(ex, "LobbyService", "Failed when leave lobby.");
        }
        finally
        {
            _isLeaving = 0;
        }
    }

    private static void _SetState(LobbyState newState)
    {
        var oldState = CurrentState;
        if (oldState == newState)
        {
            return;
        }

        CurrentState = newState;

        LogWrapper.Info("LobbyService", $"Lobby state changed from {oldState} to {newState}");

        StateChanged?.Invoke(oldState, newState);
    }

    private static void _CheckGameState(object? state)
    {
        if (!_isGameWatcherRunnable)
        {
            return;
        }

        if (_LobbyController.ScfServerEntity is null)
        {
            return;
        }

        LobbyController.IsHostInstanceAvailableAsync(_LobbyController.ScfServerEntity.EasyTier.MinecraftPort)
            .ContinueWith(async (task) =>
            {
                var isExist = await task.ConfigureAwait(false);
                if (!isExist)
                {
                    _isGameWatcherRunnable = false;
                    OnUserStopGame?.Invoke();
                }
            });
    }

    private static async Task _RunInUiAsync(Action action)
    {
        await Application.Current.Dispatcher.InvokeAsync(action);
    }
}

/// <summary>
/// Founded minecraft world information.
/// </summary>
/// <param name="Name">World name.</param>
/// <param name="Port">World share port.</param>
public record FoundWorld(string Name, int Port);