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

namespace Core.Calculation
{
    /// <summary>
    /// Solves the event assignment problem using constraint programming.
    /// Assigns students to events based on rankings, capacity constraints, and team requirements.
    /// </summary>
    public class EventAssignment
	{
        // Constants for magic numbers
        private const double TEAM_DIVISION_MULTIPLIER = 1.25;
        private const int MIN_REGIONAL_EVENTS = 1;
        private const int MAX_REGIONAL_EVENTS = 2;

        private readonly IList<EventDefinition> _events;
		private readonly IList<Student> _students;
		private readonly AssignmentParameters _parameters;
        private readonly double _maxSolveTimeSeconds;
        private readonly int[] _allEvents;
		private readonly int[] _allStudents;
		// how many students have picked each event?
		private readonly int[] _eventPickCounts;
		private IList<AssignmentRequirement> _assignmentRequirements = [];
        private IList<EventDefinition> _droppedEvents = [];
        private IList<EventDefinition> _includedEvents = [];
        private IList<EventDefinition> _twoTeams = [];

        /// <summary>
        /// Creates a new event assignment optimizer.
        /// </summary>
        /// <param name="events">The events to assign students to (must not be null or empty)</param>
        /// <param name="students">The students to assign to events (must not be null or empty)</param>
        /// <param name="parameters">Assignment parameters and constraints</param>
        /// <param name="maxSolveTimeSeconds">Maximum solver time in seconds (default: 60.0)</param>
        /// <exception cref="ArgumentNullException">Thrown when events, students, or parameters is null</exception>
        /// <exception cref="ArgumentException">Thrown when collections are empty</exception>
        public EventAssignment(IList<EventDefinition> events, IList<Student> students,
            AssignmentParameters parameters, double maxSolveTimeSeconds = 60.0)
		{
            if (events == null)
                throw new ArgumentNullException(nameof(events));
            if (students == null)
                throw new ArgumentNullException(nameof(students));
            if (parameters == null)
                throw new ArgumentNullException(nameof(parameters));

            if (events.Count == 0)
                throw new ArgumentException("Events collection cannot be empty", nameof(events));
            if (students.Count == 0)
                throw new ArgumentException("Students collection cannot be empty", nameof(students));

            _events = events;
			_students = students;
			_parameters = parameters;
            _maxSolveTimeSeconds = maxSolveTimeSeconds;
            _allEvents = Enumerable.Range(0, _events.Count).ToArray();
			_allStudents = Enumerable.Range(0, _students.Count).ToArray();
            _eventPickCounts = new int[_allEvents.Length];

            for (var i = 0; i < _events.Count; i++)
			{
				var e = _events[i];
                // Performance: Use Any() instead of Count() > 0
                _eventPickCounts[i] = _students.Count(s => s.EventRankings.Any(er => er.EventDefinition == e));
            }
		}

        /// <summary>
        /// Adds a requirement to include or exclude a specific student-event pairing.
        /// </summary>
        /// <param name="assignmentRequirement">The assignment requirement to add</param>
        public void AddAssignmentRequirement(AssignmentRequirement assignmentRequirement)
		{
            _assignmentRequirements.Add(assignmentRequirement);
		}

        /// <summary>
        /// Marks events to be excluded from the assignment solution.
        /// </summary>
        /// <param name="events">Events to drop from consideration</param>
        public void RemoveEvents(IList<EventDefinition> events)
		{
			_droppedEvents = events;
		}

        /// <summary>
        /// Forces specific events to be included in the solution.
        /// </summary>
        /// <param name="events">Events that must be included</param>
        public void SetIncludedEvents(IList<EventDefinition> events)
		{
			_includedEvents = events;
		}

        /// <summary>
        /// Allows specific events to have two teams instead of one.
        /// </summary>
        /// <param name="events">Events that can have two teams</param>
        public void AllowTwoTeams(IList<EventDefinition> events)
        {
            _twoTeams = events;
        }

        /// <summary>
        /// Solves the event assignment problem using constraint programming.
        /// Assigns students to events based on rankings, capacity constraints, and requirements.
        /// </summary>
        /// <returns>A solution containing team assignments and status</returns>
        public async Task<EventAssignmentSolution> Solve()
		{
            try
            {
                Debug.WriteLine(_parameters);

                // Create constraint programming model
                var model = new CpModel();

                // Decision variables: x[e,s] = 1 if student s is assigned to event e
                var x = new BoolVar[_allEvents.Length, _allStudents.Length];
                foreach (var e in _allEvents)
                foreach (var s in _allStudents)
                    x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");

                // Add all constraints
                AddAssignmentRequirements(model, x);
                var assignmentThresholdsList = AddEventAssignmentThresholds(model, x);
                LimitStudentAssignment(model, x);
                SetLevelOfEffort(model, x);

                if (_parameters.RequireOnSite)
                    RequireOnSiteActivity(model, x);

                if (_parameters.RequireRegional)
                    RequireRegionalEvent(model, x);

                AtMostOneIndividualEvent(model, x);
                IndividualEventsMustBeRanked(model, x);
                OptimizeStudentEventRankings(model, x);

                // Configure and run solver
                Debug.WriteLine("Starting optimization");
                var solver = new CpSolver();
                solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}";
                var cpSolverStatus = await Task.Run(() => solver.Solve(model));

                Debug.WriteLine($"Solver status: {cpSolverStatus}");

                if (cpSolverStatus == CpSolverStatus.Infeasible)
                {
                    Debug.WriteLine("Problem is infeasible - constraints cannot be satisfied");
                }

                var eventAssignmentsList = GetEventAssignments(x, solver, cpSolverStatus);

                var eventAssignmentSolution =
                    new EventAssignmentSolution
                    (
                        eventAssignmentsList.ToArray(),
                        cpSolverStatus.ToString(),
                        assignmentThresholdsList
                    );

                return eventAssignmentSolution;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Error solving event assignment: {ex}");
                throw;
            }
        }

        /// <summary>
        /// Extracts the solution from the solver results.
        /// </summary>
        private List<Team> GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus)
        {
            if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
                return [];

            var eventAssignments =
                from e in _allEvents
                let students =
                    from s in _allStudents
                    where solver.BooleanValue(x[e, s])
                    select _students[s]
                where students.Any()
                select new Team
                {
                    Identifier = _events[e].Name,
                    Event = _events[e],
                    Students = students.ToList()
                };

            return eventAssignments.ToList();
        }

        /// <summary>
        /// Objective function: Maximize student event rankings (students get higher-ranked events).
        /// </summary>
        private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x)
        {
            var maximizePicks = LinearExpr.NewBuilder();

            foreach (var s in _allStudents)
            {
                var eventPickCoefficients = GetEventPickCoefficients(_events, _students[s].EventRankings);

                foreach (var e in _allEvents)
                {
                    maximizePicks.AddTerm(x[e, s], eventPickCoefficients[e]);
                }
            }
            model.Maximize(maximizePicks);
        }

        /// <summary>
        /// Constraint: Each student must have 1-2 regional events.
        /// </summary>
        private void RequireRegionalEvent(CpModel model, BoolVar[,] x)
        {
            AddStudentConstraint(model, x,
                evt => evt.RegionalEvent,
                (m, list) => m.AddLinearConstraint(LinearExpr.Sum(list), MIN_REGIONAL_EVENTS, MAX_REGIONAL_EVENTS));
        }

        /// <summary>
        /// Constraint: Each student can have at most one individual event.
        /// </summary>
        private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x)
        {
            AddStudentConstraint(model, x,
                evt => evt.EventFormat == EventFormat.Individual,
                (m, list) => m.AddAtMostOne(list));
        }

        /// <summary>
        /// Constraint: Each student must have at least one on-site activity.
        /// </summary>
        private void RequireOnSiteActivity(CpModel model, BoolVar[,] x)
        {
            AddStudentConstraint(model, x,
                evt => evt.OnSiteActivity,
                (m, list) => m.AddAtLeastOne(list));
        }

        /// <summary>
        /// Helper method to add constraints for all students based on event filters.
        /// Reduces code duplication and improves performance by reusing the list buffer.
        /// </summary>
        private void AddStudentConstraint(CpModel model, BoolVar[,] x,
            Func<EventDefinition, bool> eventFilter, Action<CpModel, List<ILiteral>> constraintAction)
        {
            var buffer = new List<ILiteral>();
            foreach (var s in _allStudents)
            {
                buffer.Clear();
                foreach (var e in _allEvents)
                {
                    if (eventFilter(_events[e]))
                        buffer.Add(x[e, s]);
                }
                if (buffer.Count > 0)
                    constraintAction(model, buffer);
            }
        }

        /// <summary>
        /// Constraint: Individual events can only be assigned if the student ranked them.
        /// </summary>
        private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x)
        {
            var prohibitVar = new List<IntVar>(1);

            foreach (var s in _allStudents)
            {
                var student = _students[s];
                foreach (var e in _allEvents)
                {
                    var evt = _events[e];

                    if (evt.EventFormat == EventFormat.Individual
                        && student.EventRankings.Find(er => er.EventDefinition == evt) == null)
                    {
                        prohibitVar.Clear();
                        prohibitVar.Add(x[e, s]);
                        model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
                    }
                }
            }
        }

        /// <summary>
        /// Constraint: Each student's total level of effort must be within bounds.
        /// </summary>
        private void SetLevelOfEffort(CpModel model, BoolVar[,] x)
        {
            long[] eventEffortCoefficients = _events.Select(
                e => e.LevelOfEffort ?? 1L
            ).ToArray();

            foreach (var s in _allStudents)
            {
                var effortVar = new BoolVar[_allEvents.Length];

                foreach (var e in _allEvents)
                {
                    effortVar[e] = x[e, s];
                }
                var student = _students[s];
                var experienceOffset = student.TsaYear == 1 ? 1 : 0;

                var ub = _parameters.EffortUpperBound - experienceOffset;
                var lb = _parameters.EffortLowerBound;
                if (ub <= lb)
                    lb = ub - 1;

                model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) >= lb);
                model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) <= ub);
            }
        }

        /// <summary>
        /// Constraint: Limit the number of events a student is assigned.
        /// </summary>
        private void LimitStudentAssignment(CpModel model, BoolVar[,] x)
        {
            var studentCapacity = new List<IntVar>();

            foreach (var s in _allStudents)
            {
                studentCapacity.Clear();
                foreach (var e in _allEvents)
                {
                    studentCapacity.Add(x[e, s]);
                }

                model.AddLinearConstraint(
                    LinearExpr.Sum(studentCapacity), _parameters.EventsLowerBound, _parameters.EventsUpperBound);
            }
        }

        /// <summary>
        /// Adds event capacity constraints and calculates assignment thresholds.
        /// </summary>
        private List<EventAssignmentThresholds> AddEventAssignmentThresholds(CpModel model, BoolVar[,] x)
        {
            var assignmentThresholdsList = new List<EventAssignmentThresholds>();

            foreach (var e in _allEvents)
            {
                var evt = _events[e];
                var teamCount = CalculateTeamCount(evt, e);
                var (lb, ub) = CalculateEventBounds(evt, teamCount);

                AddEventConstraint(model, x, e, lb, ub);
                assignmentThresholdsList.Add(CreateThreshold(evt, teamCount, e));

                Debug.WriteLine($"{evt.Name,30}\t{evt.EventFormat,-10}\t{lb} - {ub}");
            }

            return assignmentThresholdsList;
        }

        /// <summary>
        /// Calculates how many teams should be formed for an event.
        /// </summary>
        private int CalculateTeamCount(EventDefinition evt, int eventIndex)
        {
            var eventPickCounts = _eventPickCounts[eventIndex];
            var teamDivs = eventPickCounts / (evt.MinTeamSize * TEAM_DIVISION_MULTIPLIER);

            if (_includedEvents.Contains(evt))
                return 1;

            var teamCount = (int)Math.Round(teamDivs);
            if (teamCount > evt.ChapterEligibilityCountState)
                teamCount = evt.ChapterEligibilityCountState;

            // Limit to one team for group events
            if (_parameters.LimitTeamsToOne
                && evt.EventFormat is EventFormat.Team
                && teamCount > 1
                && !_twoTeams.Contains(evt))
                teamCount = 1;

            if (_twoTeams.Contains(evt))
                teamCount = 2;

            if (_droppedEvents.Contains(evt))
                teamCount = 0;

            return teamCount;
        }

        /// <summary>
        /// Calculates the lower and upper bounds for event capacity.
        /// </summary>
        private (int lb, int ub) CalculateEventBounds(EventDefinition evt, int teamCount)
        {
            var evtMinTeamSize = evt.MinTeamSize;
            var evtMaxTeamSize = evt.MaxTeamSize;

            if (evt.EventFormat == EventFormat.Individual)
                evtMinTeamSize = 0;

            var lb = evtMinTeamSize * teamCount;
            var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit * teamCount);

            return (lb, ub);
        }

        /// <summary>
        /// Adds capacity constraint for a specific event.
        /// </summary>
        private void AddEventConstraint(CpModel model, BoolVar[,] x, int eventIndex, int lb, int ub)
        {
            var eventCapacity = new List<IntVar>();
            foreach (var s in _allStudents)
            {
                eventCapacity.Add(x[eventIndex, s]);
            }

            model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub);
            model.Minimize(LinearExpr.Sum(eventCapacity));
        }

        /// <summary>
        /// Creates a threshold object for tracking event assignment metadata.
        /// </summary>
        private EventAssignmentThresholds CreateThreshold(EventDefinition evt, int teamCount, int eventIndex)
        {
            return new EventAssignmentThresholds
            {
                Event = evt,
                TeamCount = teamCount,
                LowerBound = evt.MinTeamSize,
                UpperBound = evt.MaxTeamSize,
                StudentRankingCount = _eventPickCounts[eventIndex]
            };
        }

        /// <summary>
        /// Adds user-specified assignment requirements (include/exclude student-event pairs).
        /// </summary>
        private void AddAssignmentRequirements(CpModel model, BoolVar[,] x)
        {
            foreach (var includedAssignment in _assignmentRequirements.Where(a => a.Requirement == Requirement.Include))
            {
                var e = _events.IndexOf(includedAssignment.EventDefinition);
                var s = _students.IndexOf(includedAssignment.Student);
                model.AddAssumption(x[e, s]);
            }

            var prohibitVar = new List<IntVar>(1);
            foreach (var excludedAssignment in _assignmentRequirements.Where(a => a.Requirement == Requirement.Exclude))
            {
                var e = _events.IndexOf(excludedAssignment.EventDefinition);
                var s = _students.IndexOf(excludedAssignment.Student);

                prohibitVar.Clear();
                prohibitVar.Add(x[e, s]);
                model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
            }
        }

        /// <summary>
        /// Calculates coefficients for optimizing student preferences.
        /// Higher-ranked events get higher coefficients.
        /// </summary>
        private static long[] GetEventPickCoefficients(IList<EventDefinition> events, List<StudentEventRanking> eventRankings)
        {
            return events.Select(e =>
              {
                  var eventRank = eventRankings.FirstOrDefault(er => er.EventDefinition == e);
                  return eventRank == null
                      ? 0L
                      : StudentEventRanking.MaxRank - eventRank.Rank; // Inverse ranking
              }).ToArray();
		}
    }
}
