using System.Diagnostics;
using Core.Entities;
using Google.OrTools.Sat;

namespace Core.Calculation;

/// <summary>
/// Solves the team meeting scheduling problem using constraint programming.
/// Assigns teams to time slots while minimizing student schedule conflicts.
/// </summary>
public class TeamScheduler
{
	private readonly IList<Student> _studentObjects;
	private readonly IList<Team> _teamObjects;
	private readonly double _maxSolveTimeSeconds;

	private readonly int[] _students;
	private readonly int[] _teams;
	private readonly int[] _timeSlots;

	private readonly List<(int team1, int team2)> _scheduleSeparateTeams = [];

	/// <summary>
	/// Creates a new team scheduler instance.
	/// </summary>
	/// <param name="teams">The teams to schedule (must not be null or empty)</param>
	/// <param name="numTimeSlots">The number of available time slots (must be positive)</param>
	/// <param name="allStudents">All students participating in teams (must not be null or empty)</param>
	/// <param name="maxSolveTimeSeconds">Maximum solver time in seconds (default: 10.0)</param>
	/// <exception cref="ArgumentNullException">Thrown when teams or allStudents is null</exception>
	/// <exception cref="ArgumentException">Thrown when collections are empty or numTimeSlots is invalid</exception>
	public TeamScheduler(IEnumerable<Team> teams, int numTimeSlots, IEnumerable<Student> allStudents, double maxSolveTimeSeconds = 10.0)
	{
		if (teams == null)
			throw new ArgumentNullException(nameof(teams));
		if (allStudents == null)
			throw new ArgumentNullException(nameof(allStudents));
		if (numTimeSlots <= 0)
			throw new ArgumentException("Number of time slots must be positive", nameof(numTimeSlots));

		_teamObjects = teams.ToArray();
		_studentObjects = allStudents.ToList();
		_maxSolveTimeSeconds = maxSolveTimeSeconds;

		if (_teamObjects.Count == 0)
			throw new ArgumentException("Teams collection cannot be empty", nameof(teams));
		if (_studentObjects.Count == 0)
			throw new ArgumentException("Students collection cannot be empty", nameof(allStudents));

		_students = Enumerable.Range(0, _studentObjects.Count).ToArray();
		_teams = Enumerable.Range(0, _teamObjects.Count).ToArray();
		_timeSlots = Enumerable.Range(0, numTimeSlots).ToArray();
	}

	/// <summary>
	/// Adds a constraint requiring two teams to be scheduled in different time slots.
	/// </summary>
	/// <param name="team1">First team</param>
	/// <param name="team2">Second team</param>
	/// <exception cref="ArgumentException">Thrown when either team is not found in the scheduler</exception>
	public void ScheduleSeparate(Team team1, Team team2)
	{
		var one = _teamObjects.IndexOf(team1);
		var two = _teamObjects.IndexOf(team2);

		if (one == -1)
			throw new ArgumentException($"Team '{team1}' not found in scheduler", nameof(team1));
		if (two == -1)
			throw new ArgumentException($"Team '{team2}' not found in scheduler", nameof(team2));

		_scheduleSeparateTeams.Add((one, two));
	}

	/// <summary>
	/// Solves the team scheduling problem using constraint programming.
	/// Minimizes the number of time slots where students have conflicting team meetings.
	/// </summary>
	/// <returns>A solution containing team assignments to time slots and conflict information</returns>
	public TeamSchedulerSolution Solve()
	{
		// Create constraint programming model
		var model = new CpModel();

		// Build membership matrix: m[i,t] = 1 if student i is on team t, else 0
		var m = new int[_students.Length,_teams.Length];
		foreach (var i in _students)
		foreach (var t in _teams)
			m[i, t] = _studentObjects[i].Teams.Contains(_teamObjects[t]) ? 1 : 0;

		// Decision variables:
		// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
		var x = new IntVar[_teams.Length, _timeSlots.Length];
		foreach (var t in _teams)
		foreach (var s in _timeSlots)
			x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");

		// y[i,s] = 1 if student i has at least one meeting at time slot s, else 0
		var y = new IntVar[_students.Length, _timeSlots.Length];
		foreach (var i in _students)
		foreach (var s in _timeSlots)
			y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");

		// Constraint: each team meets exactly once
		foreach (var t in _teams)
			model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L);

		// Constraint: Link y[i,s] to whether student i has meetings at slot s
		// y[i,s] <= sum over all teams t of (m[i,t] * x[t,s])
		// This forces y[i,s] to be 1 if student i has at least one team meeting at slot s
		foreach (var i in _students)
		foreach (var s in _timeSlots)
			model.Add(
				new BoundedLinearExpression(
					y[i, s],
					LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])),
					false));

		// Objective: minimize the sum of y[i,s] values (minimize student conflicts)
		var indTimeSlotVars = LinearExpr.NewBuilder();
		foreach (var i in _students)
		foreach (var s in _timeSlots)
			indTimeSlotVars.Add(y[i, s]);
		model.Minimize(indTimeSlotVars);

		// Constraint: teams marked as "separate" must be in different time slots
		foreach (var (team1, team2) in _scheduleSeparateTeams)
		foreach (var s in _timeSlots)
			model.Add(x[team1, s] != x[team2, s]);

		// Configure and run solver
        var solver = new CpSolver();
        solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}";

        var cpSolverStatus = solver.Solve(model);
		Debug.WriteLine($"Solver status: {cpSolverStatus}");
        var timeSlotTeams = new Team[_timeSlots.Length][];

        if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
            return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());

		// Extract solution: which teams are assigned to each time slot
        foreach (var s in _timeSlots)
        {
            var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray();
            timeSlotTeams[s] = teams;
        }

        return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
	}
}