using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;

namespace Content.Server.GameTicking
{
    // This code is responsible for the assigning & picking of jobs.
    public sealed partial class GameTicker
    {
        [ViewVariables]
        private readonly List<ManifestEntry> _manifest = new();

        [ViewVariables]
        private readonly Dictionary<string, int> _spawnedPositions = new();

        private Dictionary<IPlayerSession, (string, StationId)> AssignJobs(List<IPlayerSession> availablePlayers,
            Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
        {
            var assigned = new Dictionary<IPlayerSession, (string, StationId)>();

            List<(IPlayerSession, List<string>)> GetPlayersJobCandidates(bool heads, JobPriority i)
            {
                return availablePlayers.Select(player =>
                    {
                        var profile = profiles[player.UserId];

                        var roleBans = _roleBanManager.GetJobBans(player.UserId);
                        var availableJobs = profile.JobPriorities
                            .Where(j =>
                            {
                                var (jobId, priority) = j;
                                if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job))
                                {
                                    // Job doesn't exist, probably old data?
                                    return false;
                                }

                                if (job.IsHead != heads)
                                {
                                    return false;
                                }

                                return priority == i;
                            })
                            .Where(p => roleBans != null && !roleBans.Contains(p.Key))
                            .Select(j => j.Key)
                            .ToList();

                        return (player, availableJobs);
                    })
                    .Where(p => p.availableJobs.Count != 0)
                    .ToList();
            }

            void ProcessJobs(bool heads, Dictionary<string, int> availablePositions, StationId id, JobPriority i)
            {
                var candidates = GetPlayersJobCandidates(heads, i);

                foreach (var (candidate, jobs) in candidates)
                {
                    while (jobs.Count != 0)
                    {
                        var picked = _robustRandom.Pick(jobs);

                        var openPositions = availablePositions.GetValueOrDefault(picked, 0);
                        if (openPositions == 0)
                        {
                            jobs.Remove(picked);
                            continue;
                        }

                        availablePositions[picked] -= 1;
                        assigned.Add(candidate, (picked, id));
                        break;
                    }
                }

                availablePlayers.RemoveAll(a => assigned.ContainsKey(a));
            }

            // Current strategy is to fill each station one by one.
            foreach (var (id, station) in _stationSystem.StationInfo)
            {
                // Get the ROUND-START job list.
                var availablePositions = station.MapPrototype.AvailableJobs.ToDictionary(x => x.Key, x => x.Value[0]);

                for (var i = JobPriority.High; i > JobPriority.Never; i--)
                {
                    // Process jobs possible for heads...
                    ProcessJobs(true, availablePositions, id, i);
                    // and then jobs that are not heads.
                    ProcessJobs(false, availablePositions, id, i);
                }
            }

            return assigned;
        }

        private string? PickBestAvailableJob(IPlayerSession playerSession, HumanoidCharacterProfile profile,
            StationId station)
        {
            if (station == StationId.Invalid)
                return null;

            var available = _stationSystem.StationInfo[station].JobList;

            bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId)
            {
                var roleBans = _roleBanManager.GetJobBans(playerSession.UserId);
                var filtered = profile.JobPriorities
                    .Where(p => p.Value == priority)
                    .Where(p => roleBans != null && !roleBans.Contains(p.Key))
                    .Select(p => p.Key)
                    .ToList();

                while (filtered.Count != 0)
                {
                    jobId = _robustRandom.Pick(filtered);
                    if (available.GetValueOrDefault(jobId, 0) > 0)
                    {
                        return true;
                    }

                    filtered.Remove(jobId);
                }

                jobId = default;
                return false;
            }

            if (TryPick(JobPriority.High, out var picked))
            {
                return picked;
            }

            if (TryPick(JobPriority.Medium, out picked))
            {
                return picked;
            }

            if (TryPick(JobPriority.Low, out picked))
            {
                return picked;
            }

            var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone().ToList();
            return overflows.Count != 0 ? _robustRandom.Pick(overflows) : null;
        }

        [Conditional("DEBUG")]
        private void InitializeJobController()
        {
            // Verify that the overflow role exists and has the correct name.
            var role = _prototypeManager.Index<JobPrototype>(FallbackOverflowJob);
            DebugTools.Assert(role.Name == Loc.GetString(FallbackOverflowJobName),
                "Overflow role does not have the correct name!");
        }

        private void AddSpawnedPosition(string jobId)
        {
            _spawnedPositions[jobId] = _spawnedPositions.GetValueOrDefault(jobId, 0) + 1;
        }

        private TickerJobsAvailableEvent GetJobsAvailable()
        {
            // If late join is disallowed, return no available jobs.
            if (DisallowLateJoin)
                return new TickerJobsAvailableEvent(new Dictionary<StationId, string>(), new Dictionary<StationId, Dictionary<string, int>>());

            var jobs = new Dictionary<StationId, Dictionary<string, int>>();
            var stationNames = new Dictionary<StationId, string>();

            foreach (var (id, station) in _stationSystem.StationInfo)
            {
                var list = station.JobList.ToDictionary(x => x.Key, x => x.Value);
                jobs.Add(id, list);
                stationNames.Add(id, station.Name);
            }
            return new TickerJobsAvailableEvent(stationNames, jobs);
        }

        public void UpdateJobsAvailable()
        {
            RaiseNetworkEvent(GetJobsAvailable(), Filter.Empty().AddPlayers(_playersInLobby.Keys));
        }
    }
}
