using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using Core.Entities;
using Google.OrTools.Sat;
using Microsoft.EntityFrameworkCore.Metadata;
using IntVar = Google.OrTools.Sat.IntVar;

namespace Core.Calculation
{
    public class EventAssignment
	{
		private readonly IList<EventDefinition> _events;
		private readonly IList<Student> _students;
		private readonly AssignmentParameters _parameters;
		private readonly int[] _allEvents;
		private readonly int[] _allStudents;
		// how many students have picked each eventDefinition?
		private readonly int[] _eventPickCounts;
		private IList<AssignmentRequirement> _assignmentRequirements = new List<AssignmentRequirement>();
        private IList<EventDefinition> _droppedEvents = new List<EventDefinition>();
        private IList<EventDefinition> _includedEvents = new List<EventDefinition>();
        private IList<EventDefinition> _twoTeams = new List<EventDefinition>();

        public EventAssignment(IList<EventDefinition> events, IList<Student> students, AssignmentParameters parameters)
		{
			_events = events;
			_students = students;
			_parameters = parameters;
			_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];
				
                _eventPickCounts[i] = _students.Count(s => s.EventRankings.Count(er => er.EventDefinition == e) > 0);
            }
		}

        public void AddAssignmentRequirement(AssignmentRequirement assignmentRequirement)
		{
            _assignmentRequirements.Add(assignmentRequirement);
		}

		public void RemoveEvents(IList<EventDefinition> events)
		{
			_droppedEvents = events;
		}

		public void IncludedEvents(IList<EventDefinition> events)
		{
			_includedEvents = events;
		}

        public void AllowTwoTeams(IList<EventDefinition> events)
        {
            _twoTeams = events;
        }

        public async Task<EventAssignmentSolution> Solve()
		{
			Debug.WriteLine(_parameters);

			// Model.
			var model = new CpModel();

			// Variables.
			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}]");

			AddAssignmentRequirements(model, x);

            var assignmentThresholdsList = AddEventAssignmentThresholds(model, x);

            LimitStudentAssignment(model, x);

            // set the range for level of effort
			SetLevelOfEffort(model, x);
		
			// each student should be assigned at least one on site activity
            if (_parameters.RequireOnSite)
			    RequireOnSiteActivity(model, x);
            
			// students should have at least one regional event
            if (_parameters.RequireRegional)
			    RequireRegionalEvent(model, x);

            // students should have at maximum one individual event
            AtMostOneIndividualEvent(model, x);

            //EventHasInterestedStudent(model, x);

            IndividualEventsMustBeRanked(model, x);

            OptimizeStudentEventRankings(model, x);


            Debug.WriteLine("Starting optimization");
            var solver = new CpSolver();
			var cpSolverStatus = await Task.Run(() =>  solver.Solve(model));

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

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

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

            return eventAssignmentSolution;
        }

        // Take the solution and map it back to the entities
        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();
        }

        // Maximize student event rankings
        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);
        }

        private void RequireRegionalEvent(CpModel model, BoolVar[,] x)
        {
            foreach (var s in _allStudents)
            {
                var regionalEvent = new List<ILiteral>();
                foreach (var e in _allEvents)
                {
                    if (_events[e].RegionalEvent)
                        regionalEvent.Add(x[e, s]);
                }

                // between 1 and 2 regional events
                model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2);
                regionalEvent.Clear();
            }
        }

        private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x)
        {
            foreach (var s in _allStudents)
            {
                var individualEvent = new List<ILiteral>();
                foreach (var e in _allEvents)
                {
                    if (_events[e].EventFormat == EventFormat.Individual)
                        individualEvent.Add(x[e, s]);
                }

                model.AddAtMostOne(individualEvent);
                individualEvent.Clear();
            }
        }

        private void RequireOnSiteActivity(CpModel model, BoolVar[,] x)
        {
            foreach (var s in _allStudents)
            {
                var onSiteActivity = new List<ILiteral>();
                foreach (var e in _allEvents)
                {
                    if (_events[e].OnSiteActivity)
                        onSiteActivity.Add(x[e, s]);
                }

                //if (_parameters.RequireOnSite)
                model.AddAtLeastOne(onSiteActivity);
                onSiteActivity.Clear();
            }
        }

        private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x)
        {
            foreach (var s in _allStudents)
            {
                var student = _students[s];
                foreach (var e in _allEvents)
                {
                    var evt = _events[e];

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

        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 = 0;
                switch (student.TsaYear)
                {
                    case 1: experienceOffset = 1; break;
                    default: break;
                }

                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);
            }
        }

        // Limit the number of events a student is assigned
        private void LimitStudentAssignment(CpModel model, BoolVar[,] x)
        {
            foreach (var s in _allStudents)
            {
                var studentCapacity = new List<IntVar>();
                foreach (var e in _allEvents)
                {
                    studentCapacity.Add(x[e, s]);
                }

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

        private void EventHasInterestedStudent(CpModel model, BoolVar[,] x)
        {
            foreach (var e in _allEvents)
            {
                var evt = _events[e];
                var studentInterest = new List<IntVar>();

                foreach (var s in _allStudents)
                {
                    var student = _students[s];
                    if (student.EventRankings.Find(er => er.EventDefinition == evt) != null)
                        studentInterest.Add(x[e, s]);
                }
                model.AddLinearConstraint(
                    LinearExpr.Sum(studentInterest), 1, 10);
                studentInterest.Clear();
            }
        }

        private List<EventAssignmentThresholds> AddEventAssignmentThresholds(CpModel model, BoolVar[,] x)
        {
            var assignmentThresholdsList = new List<EventAssignmentThresholds>();
            // Limit the capacity of each event
            foreach (var e in _allEvents)
            {
                var evt = _events[e];
                var eventPickCounts = _eventPickCounts[e];

                var evtMinTeamSize = evt.MinTeamSize;
                var evtMaxTeamSize = evt.MaxTeamSize;
			
                var teamDivs = eventPickCounts / (evtMinTeamSize * 1.25);
                if (_includedEvents.Contains(evt))
                    teamDivs = 1;

                //var teamsCount = (int)Math.Ceiling(teamDivs);
                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 (evt.Name == "Tech Bowl")
                    teamCount = 1;

                var eventCapacity = new List<IntVar>();
                foreach (var s in _allStudents)
                {
                    eventCapacity.Add(x[e, s]);
                }

                if (_droppedEvents != null && _droppedEvents.Contains(evt))
                    teamCount = 0;

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

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

                assignmentThresholdsList.Add(
                    new EventAssignmentThresholds
                    {
                        Event = evt,
                        TeamCount = teamCount, 
                        LowerBound = evtMinTeamSize, 
                        UpperBound = evtMaxTeamSize,
                        StudentRankingCount = eventPickCounts
                    });


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

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

                model.Minimize(LinearExpr.Sum(eventCapacity));
                eventCapacity.Clear();
            }

            return assignmentThresholdsList;
        }

        private void AddAssignmentRequirements(CpModel model, BoolVar[,] x)
        {
            foreach (var includedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Include))
            {
                var e = _events.IndexOf(includedAssignment.EventDefinition);
                var s = _students.IndexOf(includedAssignment.Student);
                model.AddAssumption(x[e, s]);
            }

            foreach (var excludedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Exclude))
            {
                var e = _events.IndexOf(excludedAssignment.EventDefinition);
                var s = _students.IndexOf(excludedAssignment.Student);
				
                var prohibitVar = new List<IntVar> { x[e, s] };

                model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
                prohibitVar.Clear();
            }
        }

        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 
                      // TODO: MaxRank can be calculated
                      : StudentEventRanking.MaxRank - eventRank.Rank; // inverse
              }).ToArray();
		}
        
        public class SolutionPrinter : CpSolverSolutionCallback
        {
            private readonly IList<EventDefinition> _events;
            private readonly IList<Student> _students;
            private readonly BoolVar[,] _eventAssignment;

            public SolutionPrinter(IList<EventDefinition> events, IList<Student> students, BoolVar[,] eventAssignment)
            {
                _events = events;
                _students = students;
                _eventAssignment = eventAssignment;
            }
            public override void OnSolutionCallback()
            {
                Console.WriteLine($"Solution ");
                foreach (var evt in Enumerable.Range(0, _events.Count))
                {
                    foreach (var student in Enumerable.Range(0, _students.Count))
                    {
                        if (Value(_eventAssignment[evt, student]) == 1L)
                        {

                        }
                    }
                }
            }
        }
    }
}