import '../modelos/alarma_musical.dart'; class ServicioProgramacionAlarmas { static const Duration toleranciaDisparoInminente = Duration(seconds: 90); DateTime? calcularProxima({ required AlarmaMusical alarma, required DateTime desde, List vacaciones = const [], List excepciones = const [], }) { if (!alarma.activa) return null; final diaBase = alarma.tipoProgramacion == TipoProgramacionAlarma.unica && alarma.fechaUnica != null ? alarma.fechaUnica! : desde; final inicio = DateTime( diaBase.year, diaBase.month, diaBase.day, alarma.hora, alarma.minuto, ); final primerCandidato = alarma.tipoProgramacion == TipoProgramacionAlarma.unica ? inicio : _sigueSiendoInminente(inicio, desde) ? inicio : inicio.add(const Duration(days: 1)); return switch (alarma.tipoProgramacion) { TipoProgramacionAlarma.unica => _sigueSiendoInminente(primerCandidato, desde) && _esValida(alarma, primerCandidato, vacaciones, excepciones) ? _normalizarInminente(primerCandidato, desde) : null, TipoProgramacionAlarma.diaria => _buscarDiaria( alarma, primerCandidato, desde, vacaciones, excepciones, ), TipoProgramacionAlarma.diasSemana => _buscarPorDiasSemana( alarma, primerCandidato, desde, vacaciones, excepciones, ), }; } DateTime calcularSnooze(DateTime desde, int minutos) { final seguro = minutos == 3 || minutos == 5 || minutos == 10 ? minutos : 5; return desde.add(Duration(minutes: seguro)); } DateTime? calcularSiguienteDespuesDeEjecucion({ required AlarmaMusical alarma, required DateTime ejecucion, List vacaciones = const [], List excepciones = const [], }) { if (!alarma.activa) return null; if (alarma.tipoProgramacion == TipoProgramacionAlarma.unica) return null; return calcularProxima( alarma: alarma.copyWith(limpiarSnooze: true), desde: ejecucion.add( toleranciaDisparoInminente + const Duration(milliseconds: 1), ), vacaciones: vacaciones, excepciones: excepciones, ); } bool estaEnVacaciones(DateTime fecha, List vacaciones) => vacaciones.any((rango) => rango.contiene(fecha)); DateTime? _buscarDiaria( AlarmaMusical alarma, DateTime candidato, DateTime desde, List vacaciones, List excepciones, ) { var actual = candidato; for (var i = 0; i < 370; i++) { if (_sigueSiendoInminente(actual, desde) && _esValida(alarma, actual, vacaciones, excepciones)) { return _normalizarInminente(actual, desde); } actual = actual.add(const Duration(days: 1)); } return null; } DateTime? _buscarPorDiasSemana( AlarmaMusical alarma, DateTime candidato, DateTime desde, List vacaciones, List excepciones, ) { if (alarma.diasSemana.isEmpty) return null; var actual = candidato; for (var i = 0; i < 370; i++) { if (alarma.diasSemana.contains(actual.weekday) && _sigueSiendoInminente(actual, desde) && _esValida(alarma, actual, vacaciones, excepciones)) { return _normalizarInminente(actual, desde); } actual = actual.add(const Duration(days: 1)); } return null; } bool _esValida( AlarmaMusical alarma, DateTime candidato, List vacaciones, List excepciones, ) { final ultimaGestionada = alarma.ultimaEjecucionGestionada; if (ultimaGestionada != null && _mismaEjecucion(ultimaGestionada, candidato)) { return false; } if (!alarma.sonarEnVacaciones && estaEnVacaciones(candidato, vacaciones)) { return false; } return !excepciones.any( (excepcion) => excepcion.alarmaId == alarma.id && _mismaEjecucion(excepcion.ejecucion, candidato), ); } bool _mismaEjecucion(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day && a.hour == b.hour && a.minute == b.minute; bool _sigueSiendoInminente(DateTime candidato, DateTime desde) => candidato.isAfter(desde) || desde.difference(candidato) <= toleranciaDisparoInminente; DateTime _normalizarInminente(DateTime candidato, DateTime desde) => candidato.isAfter(desde) ? candidato : desde.add(const Duration(seconds: 2)); }