using Core.Entities;

namespace Core.Calculation;

public class UnassignedStudentScheduler
{
	private readonly Student[] _students;
	private readonly Team[] _allTeams;
	private readonly IList<Team>[] _timeSlots;

    public UnassignedStudentScheduler(Team[] allTeams, TeamScheduleTimeSlot[] timeslots) 
        : this(allTeams, timeslots.Select(e => e.Teams).ToArray())
    { }

    public UnassignedStudentScheduler(Team[] allTeams, Team[][] timeslots)
	{
		_allTeams = allTeams;
		_students = allTeams.SelectMany(t => t.Students).Distinct().ToArray();
		_timeSlots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToList()).ToArray<IList<Team>>();
	}
	public static IEnumerable<Student> UnassignedStudents(IList<Student> students, IList<Team> timeSlot) 
		=> students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s));
 
	public static IEnumerable<Student>[] UnassignedStudents(IList<Student> students, IList<Team>[] schedule) 
		=> schedule.Select(ts => UnassignedStudents(students, ts)).ToArray();

	public Team[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy)
    {
        var assignments = scheduleStrategy switch
        {
            UnassignedScheduleStrategy.BiggestGroup => ScheduleStrategy(GetAvailableTeams_BiggestGroup),
            UnassignedScheduleStrategy.IndividualEvents => ScheduleStrategy(GetAvailableTeams_Individual),
            UnassignedScheduleStrategy.AnyNotMeetingAlready => ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready),
            UnassignedScheduleStrategy.Any => ScheduleStrategy(GetAvailableTeams_Any),
            UnassignedScheduleStrategy.LevelOfEffort => ScheduleStrategy(GetAvailableTeams_LevelOfEffort),
            _ => throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null)
        };

        return assignments;
    }

	public Team[] ScheduleStrategy(Func<IEnumerable<Team>, IEnumerable<Student>, IEnumerable<Team>> availableTeamSelector)
	{
		// Find stuff for unassigned students in each timeslot
		var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList();
        var additions = new List<Team>();
        foreach (var slot in _timeSlots)
		{
			var unassigned = UnassignedStudents(_students, slot).ToList();
			while (unassigned.Count > 0)
			{
				var assignedStudents = slot.SelectMany(t => t.Students).Distinct();

				var availableTeams = availableTeamSelector(scheduledTeams, assignedStudents);

				var teamToAdd = availableTeams.FirstOrDefault();
				if (teamToAdd == null)
					break;
				
				slot.Add(teamToAdd);
				scheduledTeams.Add(teamToAdd);
                additions.Add(teamToAdd);

                foreach (var student in teamToAdd.Students)
					unassigned.Remove(student);
			}
		}

        return additions.ToArray();
    }

	// find teams where several unassigned students can work together
	private IEnumerable<Team> GetAvailableTeams_BiggestGroup(
		IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
		_allTeams
			.Where(t => scheduledTeams.All(st => st.Id != t.Id))
			.Select(t => t.CloneWithOmittedStudents(assignedStudents))
			.Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual
			//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
			.OrderByDescending(t => t.Students.Count); // select descending greatest number of students assigned
			//.ThenBy(t => Random.Shared.Next()); // todo: sort by student historic record of eventDefinition assignment

	// find individual events unassigned students can work on
	private IEnumerable<Team> GetAvailableTeams_Individual(
		IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
		_allTeams
			.Where(t => scheduledTeams.All(st => st.Id != t.Id))
			.Where(t => t.Event.EventFormat == EventFormat.Individual || t.Students.Count == 1)
			.Select(t => t.CloneWithOmittedStudents(assignedStudents))
			.Where(t => t.Students.Count > 0);

	// find any unassigned eventDefinition students can work on
	private IEnumerable<Team> GetAvailableTeams_AnyNotMeetingAlready(
		IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
		_allTeams
			.Where(t => scheduledTeams.All(st => st.Id != t.Id))
			.Select(t => t.CloneWithOmittedStudents(assignedStudents))
			.Where(t => t.Students.Count > 0);

	private IEnumerable<Team> GetAvailableTeams_Any(
		IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
		_allTeams
			.Select(t => t.CloneWithOmittedStudents(assignedStudents))
			.Where(t => t.Students.Count > 0);


	// find teams where several unassigned students can work together
	private IEnumerable<Team> GetAvailableTeams_LevelOfEffort(
		IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
		_allTeams
			.Where(t => scheduledTeams.All(st => st.Id != t.Id))
			.Select(t => t.CloneWithOmittedStudents(assignedStudents))
			.Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual
			//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
			.OrderByDescending(t => t.Event.LevelOfEffort); // select descending greatest number of students assigned

	//add team to another timeslot, if any available
	public IList<Team>[] AddAdditionalTimeSlot(Team team)
	{
		// sort by how many teammembers are already in that timeslot, descending
		foreach (var timeslot in _timeSlots.OrderBy(ts => ts.SelectMany(t => t.Students).Count(team.Students.Contains)))
		{
			if (timeslot.Any(t => t.Identifier == team.Identifier))
				continue;
			timeslot.Add(team);
			break;
		}

		return _timeSlots;
	}

	// Keep the current events rolling for the other time slots
	public IList<Team>[] ExtendEvents()
	{
		var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList();

		// get all the students in each time slot
		var timeslotStudents = _timeSlots.Select(ts => ts.SelectMany(t => t.Students).Distinct()).ToArray();

		// clone teams from timeslot for each other timeslot, removing the students

		//var copiedTeams = new Dictionary<int, IList<Team>>();

		for (var i = 0; i < _timeSlots.Length; i++)
		{
			var sourceTimeslot = _timeSlots[i];

			for (var j = 0; j < _timeSlots.Length; j++)
			{
				if (i == j)
					continue;
				var targetTimeslot = _timeSlots[j];
				var targetTimeslotStudents = targetTimeslot.SelectMany(t => t.Students).Distinct();
				var clonedTeams = sourceTimeslot.Select(t => t.CloneWithOmittedStudents(targetTimeslotStudents));
				foreach (var clonedTeam in clonedTeams.Where(t => t.Students.Any()))
				{
					targetTimeslot.Add(clonedTeam);
				}
			}
		}

		return _timeSlots;
	}
}