using Asp.Versioning; using Cronos; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Mirea.Api.DataAccess.Persistence; using Mirea.Api.Dto.Common; using Mirea.Api.Dto.Responses.Configuration; using Mirea.Api.Endpoint.Common.Exceptions; using Mirea.Api.Endpoint.Common.MapperDto; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Configuration.Model; using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings; using Mirea.Api.Endpoint.Sync; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Controllers.V1.Configuration; [ApiVersion("1.0")] public class ScheduleController(ILogger logger, IOptionsSnapshot config, UberDbContext dbContext, IServiceProvider provider) : ConfigurationBaseController { /// /// Retrieves the cron update schedule and calculates the next scheduled tasks based on the provided depth. /// /// The depth of the next tasks to retrieve. /// Cron expression and the list of next scheduled task dates. [HttpGet("CronUpdateSchedule")] public ActionResult CronUpdateSchedule([FromQuery][Range(0, 10)] int depth = 5) { var cronExpression = CronExpression.Parse(config.Value.ScheduleSettings!.CronUpdateSchedule); var nextTasks = config.Value.ScheduleSettings!.CronUpdateSkipDateList.GetNextTask(cronExpression, depth); return new CronUpdateScheduleResponse() { Cron = config.Value.ScheduleSettings!.CronUpdateSchedule, NextStart = nextTasks.Select(x => DateTime.SpecifyKind(x.DateTime, DateTimeKind.Local)).ToList() }; } /// /// Updates the cron update schedule with the provided cron expression. /// /// The cron expression to set as the new schedule. /// Cron expression and the list of next scheduled task dates. /// Thrown if the provided cron expression is invalid. [HttpPost("CronUpdateSchedule")] public ActionResult CronUpdateSchedule([FromQuery] string cron) { cron = cron.Trim(); if (!CronExpression.TryParse(cron, CronFormat.Standard, out _)) throw new ControllerArgumentException("Incorrect cron value."); if (config.Value.ScheduleSettings!.CronUpdateSchedule == cron) return CronUpdateSchedule(); config.Value.ScheduleSettings!.CronUpdateSchedule = cron; config.Value.SaveSetting(); return CronUpdateSchedule(); } /// /// Retrieves the start term date from the configuration. /// /// Start term date. [HttpGet("StartTerm")] public ActionResult StartTerm() => config.Value.ScheduleSettings!.StartTerm; /// /// Updates the start term date in the configuration. /// /// The new start term date to set. /// If true, forces an update by deleting all existing lessons. /// Success or failure. /// Thrown if the start term date is more than 6 months in the past or future. [HttpPost("StartTerm")] public ActionResult StartTerm([FromQuery] DateOnly startTerm, [FromQuery] bool force = false) { var differentByTime = DateTime.Now - startTerm.ToDateTime(new TimeOnly(0, 0, 0)); if (differentByTime > TimeSpan.FromDays(190) || differentByTime.Multiply(-1) > TimeSpan.FromDays(190)) throw new ControllerArgumentException("The semester can't start more than 6 months from now, and it can't have started more than 6 months ago either."); config.Value.ScheduleSettings!.StartTerm = startTerm; config.Value.SaveSetting(); if (!force) return Ok(); logger.LogWarning("A force update is being performed at the beginning of the semester (all classes will be deleted)."); dbContext.Lessons.RemoveRange(dbContext.Lessons.ToList()); dbContext.SaveChanges(); return Ok(); } /// /// Retrieves the list of cron update skip dates filtered by the current date. /// /// Cron update skip dates. [HttpGet("CronUpdateSkip")] public ActionResult> CronUpdateSkip() { var generalConfig = config.Value; generalConfig.ScheduleSettings!.CronUpdateSkipDateList = generalConfig.ScheduleSettings.CronUpdateSkipDateList.Filter(); generalConfig.SaveSetting(); return generalConfig.ScheduleSettings!.CronUpdateSkipDateList .ConvertToDto(); } /// /// Updates the list of cron update skip dates in the configuration. /// /// The list of cron update skip dates to set. /// Success or failure. /// Thrown if the provided list of cron update skip dates is invalid. [HttpPost("CronUpdateSkip")] public ActionResult CronUpdateSkip([FromBody] List cronUpdateDate) { List result; try { result = cronUpdateDate.ConvertFromDto(); } catch (ArgumentException ex) { throw new ControllerArgumentException(ex.Message); } config.Value.ScheduleSettings!.CronUpdateSkipDateList = result.Filter(); config.Value.SaveSetting(); return Ok(); } /// /// Uploads schedule files and initiates synchronization. /// /// The list of schedule files to upload. /// The default campus for each uploaded file. Must match the number of files. /// If true, removes all existing lessons before synchronization. Default is false. /// Success or failure. /// /// Thrown if: /// - No files are provided. /// - The number of default campuses does not match the number of files. /// - Any default campus is null or empty. /// [HttpPost("Upload")] public async Task UploadScheduleFiles(List? files, [FromQuery] string[]? defaultCampus, [FromQuery] bool force = false) { if (files == null || files.Count == 0) throw new ControllerArgumentException("No files were found."); if (defaultCampus == null || files.Count != defaultCampus.Length) throw new ControllerArgumentException("No default campuses are specified for the file."); if (defaultCampus.Any(string.IsNullOrEmpty)) throw new ControllerArgumentException("Each file should have a default campus."); var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); if (!Directory.Exists(tempDirectory)) Directory.CreateDirectory(tempDirectory); List<(string, string)> filePaths = []; for (var i = 0; i < files.Count; i++) { if (files[i].Length <= 0) continue; var filePath = Path.Combine(tempDirectory, files[i].FileName); await using var stream = new FileStream(filePath, FileMode.Create); await files[i].CopyToAsync(stream); filePaths.Add((filePath, defaultCampus[i])); } if (force) { dbContext.Lessons.RemoveRange(await dbContext.Lessons.ToListAsync()); await dbContext.SaveChangesAsync(); } var scopeFactory = provider.GetRequiredService(); ThreadPool.QueueUserWorkItem(async void (_) => { try { using var scope = scopeFactory.CreateScope(); var sync = (ScheduleSynchronizer)ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, typeof(ScheduleSynchronizer)); await sync.StartSync(filePaths, CancellationToken.None); } catch (Exception ex) { Console.WriteLine(ex.Message); } }); return Ok(); } }