using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration;
using Content.Server.Administration.Managers;
using Content.Server.Afk;
using Content.Server.Chat.Managers;
using Content.Server.Maps;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Voting;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;


namespace Content.Server.Voting.Managers
{
    public sealed partial class VoteManager : IVoteManager
    {
        [Dependency] private readonly IServerNetManager _netManager = default!;
        [Dependency] private readonly IConfigurationManager _cfg = default!;
        [Dependency] private readonly IGameTiming _timing = default!;
        [Dependency] private readonly IPlayerManager _playerManager = default!;
        [Dependency] private readonly IChatManager _chatManager = default!;
        [Dependency] private readonly IAdminManager _adminMgr = default!;
        [Dependency] private readonly IRobustRandom _random = default!;
        [Dependency] private readonly IAfkManager _afkManager = default!;
        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
        [Dependency] private readonly IGameMapManager _gameMapManager = default!;

        private int _nextVoteId = 1;

        private readonly Dictionary<int, VoteReg> _votes = new();
        private readonly Dictionary<int, VoteHandle> _voteHandles = new();

        private readonly Dictionary<StandardVoteType, TimeSpan> _standardVoteTimeout = new();
        private readonly Dictionary<NetUserId, TimeSpan> _voteTimeout = new();
        private readonly HashSet<IPlayerSession> _playerCanCallVoteDirty = new();
        private readonly StandardVoteType[] _standardVoteTypeValues = Enum.GetValues<StandardVoteType>();

        public void Initialize()
        {
            _netManager.RegisterNetMessage<MsgVoteData>();
            _netManager.RegisterNetMessage<MsgVoteCanCall>();

            _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
            _adminMgr.OnPermsChanged += AdminPermsChanged;

            _cfg.OnValueChanged(CCVars.VoteEnabled, value => {
                DirtyCanCallVoteAll();
            });

            foreach (var kvp in _voteTypesToEnableCVars)
            {
                _cfg.OnValueChanged(kvp.Value, value => {
                    DirtyCanCallVoteAll();
                });
            }
        }

        private void AdminPermsChanged(AdminPermsChangedEventArgs obj)
        {
            DirtyCanCallVote(obj.Player);
        }

        private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
        {
            if (e.NewStatus == SessionStatus.InGame)
            {
                // Send current votes to newly connected players.
                foreach (var voteReg in _votes.Values)
                {
                    SendSingleUpdate(voteReg, e.Session);
                }

                DirtyCanCallVote(e.Session);
            }
            else if (e.NewStatus == SessionStatus.Disconnected)
            {
                // Clear votes from disconnected players.
                foreach (var voteReg in _votes.Values)
                {
                    CastVote(voteReg, e.Session, null);
                }
            }
        }

        private void CastVote(VoteReg v, IPlayerSession player, int? option)
        {
            if (!IsValidOption(v, option))
                throw new ArgumentOutOfRangeException(nameof(option), "Invalid vote option ID");

            if (v.CastVotes.TryGetValue(player, out var existingOption))
            {
                v.Entries[existingOption].Votes -= 1;
            }

            if (option != null)
            {
                v.Entries[option.Value].Votes += 1;
                v.CastVotes[player] = option.Value;
            }
            else
            {
                v.CastVotes.Remove(player);
            }

            v.VotesDirty.Add(player);
            v.Dirty = true;
        }

        private bool IsValidOption(VoteReg voteReg, int? option)
        {
            return option == null || option >= 0 && option < voteReg.Entries.Length;
        }

        public void Update()
        {
            // Handle active votes.
            var remQueue = new RemQueue<int>();
            foreach (var v in _votes.Values)
            {
                // Logger.Debug($"{_timing.ServerTime}");
                if (_timing.RealTime >= v.EndTime)
                    EndVote(v);

                if (v.Finished)
                    remQueue.Add(v.Id);

                if (v.Dirty)
                    SendUpdates(v);
            }

            foreach (var id in remQueue)
            {
                _votes.Remove(id);
                _voteHandles.Remove(id);
            }

            // Handle player timeouts.
            var timeoutRemQueue = new RemQueue<NetUserId>();
            foreach (var (userId, timeout) in _voteTimeout)
            {
                if (timeout < _timing.RealTime)
                    timeoutRemQueue.Add(userId);
            }

            foreach (var userId in timeoutRemQueue)
            {
                _voteTimeout.Remove(userId);

                if (_playerManager.TryGetSessionById(userId, out var session))
                    DirtyCanCallVote(session);
            }

            // Handle standard vote timeouts.
            var stdTimeoutRemQueue = new RemQueue<StandardVoteType>();
            foreach (var (type, timeout) in _standardVoteTimeout)
            {
                if (timeout < _timing.RealTime)
                    stdTimeoutRemQueue.Add(type);
            }

            foreach (var type in stdTimeoutRemQueue)
            {
                _standardVoteTimeout.Remove(type);

                DirtyCanCallVoteAll();
            }

            // Handle dirty canCallVotes.
            foreach (var dirtyPlayer in _playerCanCallVoteDirty)
            {
                if (dirtyPlayer.Status != SessionStatus.Disconnected)
                    SendUpdateCanCallVote(dirtyPlayer);
            }

            _playerCanCallVoteDirty.Clear();
        }

        public IVoteHandle CreateVote(VoteOptions options)
        {
            var id = _nextVoteId++;

            var entries = options.Options.Select(o => new VoteEntry(o.data, o.text)).ToArray();

            var start = _timing.RealTime;
            var end = start + options.Duration;
            var reg = new VoteReg(id, entries, options.Title, options.InitiatorText,
                options.InitiatorPlayer, start, end);

            var handle = new VoteHandle(this, reg);

            _votes.Add(id, reg);
            _voteHandles.Add(id, handle);

            if (options.InitiatorPlayer != null)
            {
                var timeout = options.InitiatorTimeout ?? options.Duration * 2;
                _voteTimeout[options.InitiatorPlayer.UserId] = _timing.RealTime + timeout;
            }

            DirtyCanCallVoteAll();

            return handle;
        }

        private void SendUpdates(VoteReg v)
        {
            foreach (var player in _playerManager.ServerSessions)
            {
                SendSingleUpdate(v, player);
            }

            v.VotesDirty.Clear();
            v.Dirty = false;
        }

        private void SendSingleUpdate(VoteReg v, IPlayerSession player)
        {
            var msg = _netManager.CreateNetMessage<MsgVoteData>();

            msg.VoteId = v.Id;
            msg.VoteActive = !v.Finished;

            if (!v.Finished)
            {
                msg.VoteTitle = v.Title;
                msg.VoteInitiator = v.InitiatorText;
                msg.StartTime = v.StartTime;
                msg.EndTime = v.EndTime;
            }

            if (v.CastVotes.TryGetValue(player, out var cast))
            {
                // Only send info for your vote IF IT CHANGED.
                // Otherwise there would be a reconciliation b*g causing the UI to jump back and forth.
                // (votes are not in simulation so can't use normal prediction/reconciliation sadly).
                var dirty = v.VotesDirty.Contains(player);
                msg.IsYourVoteDirty = dirty;
                if (dirty)
                {
                    msg.YourVote = (byte) cast;
                }
            }

            msg.Options = new (ushort votes, string name)[v.Entries.Length];
            for (var i = 0; i < msg.Options.Length; i++)
            {
                ref var entry = ref v.Entries[i];
                msg.Options[i] = ((ushort) entry.Votes, entry.Text);
            }

            player.ConnectedClient.SendMessage(msg);
        }

        private void DirtyCanCallVoteAll()
        {
            _playerCanCallVoteDirty.UnionWith(_playerManager.ServerSessions);
        }

        private void SendUpdateCanCallVote(IPlayerSession player)
        {
            var msg = _netManager.CreateNetMessage<MsgVoteCanCall>();
            msg.CanCall = CanCallVote(player, null, out var isAdmin, out var timeSpan);
            msg.WhenCanCallVote = timeSpan;

            if (isAdmin)
            {
                msg.VotesUnavailable = Array.Empty<(StandardVoteType, TimeSpan)>();
            }
            else
            {
                var votesUnavailable = new List<(StandardVoteType, TimeSpan)>();
                foreach (var v in _standardVoteTypeValues)
                {
                    if (CanCallVote(player, v, out var _isAdmin, out var typeTimeSpan))
                        continue;
                    votesUnavailable.Add((v, typeTimeSpan));
                }
                msg.VotesUnavailable = votesUnavailable.ToArray();
            }

            _netManager.ServerSendMessage(msg, player.ConnectedClient);
        }

        private bool CanCallVote(
            IPlayerSession initiator,
            StandardVoteType? voteType,
            out bool isAdmin,
            out TimeSpan timeSpan)
        {
            isAdmin = false;
            timeSpan = default;

            // Admins can always call votes.
            if (_adminMgr.HasAdminFlag(initiator, AdminFlags.Admin))
            {
                isAdmin = true;
                return true;
            }

            // If voting is disabled, block votes.
            if (!_cfg.GetCVar(CCVars.VoteEnabled))
                return false;
            // Specific standard vote types can be disabled with cvars.
            if ((voteType != null) && _voteTypesToEnableCVars.TryGetValue(voteType.Value, out var cvar) && !_cfg.GetCVar(cvar))
                return false;

            // Cannot start vote if vote is already active (as non-admin).
            if (_votes.Count != 0)
                return false;

            // Standard vote on timeout, no calling.
            // Ghosts I understand you're dead but stop spamming the restart vote bloody hell.
            if (voteType != null && _standardVoteTimeout.TryGetValue(voteType.Value, out timeSpan))
                return false;

            return !_voteTimeout.TryGetValue(initiator.UserId, out timeSpan);
        }

        public bool CanCallVote(IPlayerSession initiator, StandardVoteType? voteType = null)
        {
            return CanCallVote(initiator, voteType, out _, out _);
        }

        private void EndVote(VoteReg v)
        {
            if (v.Finished)
            {
                return;
            }

            // Find winner or stalemate.
            var winners = v.Entries
                .GroupBy(e => e.Votes)
                .OrderByDescending(g => g.Key)
                .First()
                .Select(e => e.Data)
                .ToImmutableArray();

            v.Finished = true;
            v.Dirty = true;
            var args = new VoteFinishedEventArgs(winners.Length == 1 ? winners[0] : null, winners);
            v.OnFinished?.Invoke(_voteHandles[v.Id], args);
            DirtyCanCallVoteAll();
        }

        private void CancelVote(VoteReg v)
        {
            if (v.Cancelled)
                return;

            v.Cancelled = true;
            v.Finished = true;
            v.Dirty = true;
            v.OnCancelled?.Invoke(_voteHandles[v.Id]);
            DirtyCanCallVoteAll();
        }

        public IEnumerable<IVoteHandle> ActiveVotes => _voteHandles.Values;

        public bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote)
        {
            if (_voteHandles.TryGetValue(voteId, out var vHandle))
            {
                vote = vHandle;
                return true;
            }

            vote = default;
            return false;
        }

        private void DirtyCanCallVote(IPlayerSession player)
        {
            _playerCanCallVoteDirty.Add(player);
        }

        #region Preset Votes

        private void WirePresetVoteInitiator(VoteOptions options, IPlayerSession? player)
        {
            if (player != null)
            {
                options.SetInitiator(player);
            }
            else
            {
                options.InitiatorText = Loc.GetString("ui-vote-initiator-server");
            }
        }

        #endregion

        #region Vote Data

        private sealed class VoteReg
        {
            public readonly int Id;
            public readonly Dictionary<IPlayerSession, int> CastVotes = new();
            public readonly VoteEntry[] Entries;
            public readonly string Title;
            public readonly string InitiatorText;
            public readonly TimeSpan StartTime;
            public readonly TimeSpan EndTime;
            public readonly HashSet<IPlayerSession> VotesDirty = new();

            public bool Cancelled;
            public bool Finished;
            public bool Dirty = true;

            public VoteFinishedEventHandler? OnFinished;
            public VoteCancelledEventHandler? OnCancelled;
            public IPlayerSession? Initiator { get; }

            public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText,
                IPlayerSession? initiator, TimeSpan start, TimeSpan end)
            {
                Id = id;
                Entries = entries;
                Title = title;
                InitiatorText = initiatorText;
                Initiator = initiator;
                StartTime = start;
                EndTime = end;
            }
        }

        private struct VoteEntry
        {
            public object Data;
            public string Text;
            public int Votes;

            public VoteEntry(object data, string text)
            {
                Data = data;
                Text = text;
                Votes = 0;
            }
        }

        #endregion

        #region IVoteHandle API surface

        private sealed class VoteHandle : IVoteHandle
        {
            private readonly VoteManager _mgr;
            private readonly VoteReg _reg;

            public int Id => _reg.Id;
            public string Title => _reg.Title;
            public string InitiatorText => _reg.InitiatorText;
            public bool Finished => _reg.Finished;
            public bool Cancelled => _reg.Cancelled;

            public IReadOnlyDictionary<object, int> VotesPerOption { get; }

            public event VoteFinishedEventHandler? OnFinished
            {
                add => _reg.OnFinished += value;
                remove => _reg.OnFinished -= value;
            }

            public event VoteCancelledEventHandler? OnCancelled
            {
                add => _reg.OnCancelled += value;
                remove => _reg.OnCancelled -= value;
            }

            public VoteHandle(VoteManager mgr, VoteReg reg)
            {
                _mgr = mgr;
                _reg = reg;

                VotesPerOption = new VoteDict(reg);
            }

            public bool IsValidOption(int optionId)
            {
                return _mgr.IsValidOption(_reg, optionId);
            }

            public void CastVote(IPlayerSession session, int? optionId)
            {
                _mgr.CastVote(_reg, session, optionId);
            }

            public void Cancel()
            {
                _mgr.CancelVote(_reg);
            }

            private sealed class VoteDict : IReadOnlyDictionary<object, int>
            {
                private readonly VoteReg _reg;

                public VoteDict(VoteReg reg)
                {
                    _reg = reg;
                }

                public IEnumerator<KeyValuePair<object, int>> GetEnumerator()
                {
                    return _reg.Entries.Select(e => KeyValuePair.Create(e.Data, e.Votes)).GetEnumerator();
                }

                IEnumerator IEnumerable.GetEnumerator()
                {
                    return GetEnumerator();
                }

                public int Count => _reg.Entries.Length;

                public bool ContainsKey(object key)
                {
                    return TryGetValue(key, out _);
                }

                public bool TryGetValue(object key, out int value)
                {
                    var entry = _reg.Entries.FirstOrNull(a => a.Data.Equals(key));
                    if (entry != null)
                    {
                        value = entry.Value.Votes;
                        return true;
                    }

                    value = default;
                    return false;
                }

                public int this[object key]
                {
                    get
                    {
                        if (!TryGetValue(key, out var votes))
                        {
                            throw new KeyNotFoundException();
                        }

                        return votes;
                    }
                }

                public IEnumerable<object> Keys => _reg.Entries.Select(c => c.Data);
                public IEnumerable<int> Values => _reg.Entries.Select(c => c.Votes);
            }
        }

        #endregion
    }
}
