import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; import { BloodPressureService } from '../blood-pressure/blood-pressure.service'; import { SymptomsService } from '../symptoms/symptoms.service'; import { UserProfileService } from '../user-profile/user-profile.service'; import { MonitoringState, FollowUpTaskType, FollowUpTaskStatus, SymptomType, } from '@prisma/client'; import { EmergencyService } from 'src/emergency/emergency.service'; @Injectable() export class MonitoringEngineService { private readonly logger = new Logger(MonitoringEngineService.name); constructor( private readonly prisma: PrismaService, @Inject(forwardRef(() => BloodPressureService)) @Inject(forwardRef(() => EmergencyService)) private readonly bloodPressureService: BloodPressureService, private readonly symptomsService: SymptomsService, private readonly userProfileService: UserProfileService, private readonly emergencyService: EmergencyService, ) {} async evaluate(userId: string) { const now = new Date(); let currentState: MonitoringState = MonitoringState.NORMAL; let reason: string | null = null; let nextAction: string | null = null; let followUpTask = null; // 1. Aggregate historical data const last48Hours = new Date(now.getTime() - 48 * 60 * 60 * 1000); const last72Hours = new Date(now.getTime() - 72 * 60 * 60 * 1000); const recentBPs = await this.bloodPressureService.getRecentReadings( userId, 48, ); const latestBP = recentBPs.length > 0 ? recentBPs[0] : null; const recentSymptoms = await this.symptomsService.getRecentSymptoms( userId, 72, ); const symptomTypes = new Set(recentSymptoms.map((s) => s.symptomType)); // 2. Detect patterns (TREND ENGINE) // Rule B: Severe BP if (latestBP && (latestBP.systolic >= 160 || latestBP.diastolic >= 110)) { currentState = MonitoringState.EMERGENCY; reason = 'Severe blood pressure reading detected.'; nextAction = 'Seek immediate medical attention.'; } // Rule C: BP + symptom combination if ( latestBP && (latestBP.systolic >= 140 || latestBP.diastolic >= 90) && (symptomTypes.has(SymptomType.HEADACHE) || symptomTypes.has(SymptomType.BLURRED_VISION) || symptomTypes.has(SymptomType.UPPER_ABDOMINAL_PAIN)) ) { currentState = MonitoringState.EMERGENCY; reason = 'Elevated blood pressure with severe symptoms.'; nextAction = 'Seek immediate medical attention.'; } // If already EMERGENCY, we stop here for escalation if (currentState === MonitoringState.EMERGENCY) { await this.updateMonitoringState(userId, currentState, reason); return { state: currentState, message: reason, nextAction, followUp: null, }; } // Rule A: Repeated high BP (>=3 readings where systolic >=140 OR diastolic >=90 within 48h) const elevatedReadings = recentBPs.filter( (bp) => bp.systolic >= 140 || bp.diastolic >= 90, ); if (elevatedReadings.length >= 3) { currentState = MonitoringState.URGENT; reason = 'Repeated high blood pressure readings within 48 hours.'; nextAction = 'Contact your healthcare provider within 24 hours.'; } // Rule D: Rising trend (basic version) - last 3 readings show increasing systolic or diastolic if (recentBPs.length >= 3) { const sortedBPs = recentBPs .slice(0, 3) .sort((a, b) => a.recordedAt.getTime() - b.recordedAt.getTime()); // Oldest first const isSystolicRising = sortedBPs[0].systolic < sortedBPs[1].systolic && sortedBPs[1].systolic < sortedBPs[2].systolic; const isDiastolicRising = sortedBPs[0].diastolic < sortedBPs[1].diastolic && sortedBPs[1].diastolic < sortedBPs[2].diastolic; if (isSystolicRising || isDiastolicRising) { // Cast to avoid TS narrowing issues when comparing against the "current" state const checkState = currentState as MonitoringState; if (checkState === MonitoringState.NORMAL) { currentState = MonitoringState.MONITOR; reason = 'Rising trend in blood pressure detected.'; nextAction = 'Continue monitoring closely.'; } else if (checkState === MonitoringState.MONITOR) { currentState = MonitoringState.URGENT; reason = 'Significant rising trend in blood pressure detected.'; nextAction = 'Contact your healthcare provider within 24 hours.'; } } } // Initial high reading (NORMAL -> MONITOR) if ( (currentState as MonitoringState) === MonitoringState.NORMAL && latestBP && (latestBP.systolic >= 140 || latestBP.diastolic >= 90) ) { currentState = MonitoringState.MONITOR; reason = 'First elevated blood pressure reading.'; nextAction = 'Recheck your blood pressure in 4 hours.'; } // 3. Introduce STATE MACHINE & 4. STATE TRANSITIONS const previousStateRecord = await this.prisma.monitoringStateRecord.findUnique( { where: { userId }, }, ); if (previousStateRecord) { // Transition from Any -> NORMAL (after consistent normal readings) // This is a simplified check. A more robust solution would involve checking a longer history of normal readings. const checkState = currentState as MonitoringState; if ( previousStateRecord.currentState !== MonitoringState.NORMAL && checkState === MonitoringState.NORMAL && recentBPs.every((bp) => bp.systolic < 140) && recentBPs.every((bp) => bp.diastolic < 90) && recentSymptoms.length === 0 ) { currentState = MonitoringState.NORMAL; reason = 'Consistent normal readings and no concerning symptoms.'; nextAction = 'Continue routine monitoring.'; } } // 5. FOLLOW-UP SYSTEM (CRITICAL) // When BP >= 140/90: Create RECHECK task: dueAt = now + 4 hours if ( latestBP && (latestBP.systolic >= 140 || latestBP.diastolic >= 90) && (currentState as MonitoringState) !== MonitoringState.EMERGENCY ) { followUpTask = await this.createFollowUpTask( userId, FollowUpTaskType.RECHECK, new Date(now.getTime() + 4 * 60 * 60 * 1000), // 4 hours from now latestBP.id, ); nextAction = 'Please recheck your blood pressure in 4 hours.'; } // When state = URGENT: Create SEEK_CARE task: dueAt = now + 24 hours if ((currentState as MonitoringState) === MonitoringState.URGENT) { followUpTask = await this.createFollowUpTask( userId, FollowUpTaskType.SEEK_CARE, new Date(now.getTime() + 24 * 60 * 60 * 1000), // 24 hours from now latestBP?.id, // Link to the latest BP if available ); nextAction = 'Contact your healthcare provider within 24 hours.'; } await this.updateMonitoringState(userId, currentState, reason); return { state: currentState, message: reason, nextAction, followUp: followUpTask, }; } async getMonitoringState(userId: string) { return this.prisma.monitoringStateRecord.findUnique({ where: { userId }, }); } async getPendingFollowUpTasks(userId: string) { return this.prisma.followUpTask.findMany({ where: { userId, status: FollowUpTaskStatus.PENDING, }, orderBy: { dueAt: 'asc', }, }); } async completeFollowUpTask(taskId: string) { return this.prisma.followUpTask.update({ where: { id: taskId }, data: { status: FollowUpTaskStatus.COMPLETED }, }); } // private async updateMonitoringState( // userId: string, // newState: MonitoringState, // reason: string | null, // ) { // return this.prisma.monitoringStateRecord.upsert({ // where: { userId }, // update: { // currentState: newState, // reason, // lastUpdatedAt: new Date(), // }, // create: { // userId, // currentState: newState, // reason, // lastUpdatedAt: new Date(), // }, // }); // } private async updateMonitoringState( userId: string, newState: MonitoringState, reason: string | null, ) { const previous = await this.prisma.monitoringStateRecord.findUnique({ where: { userId }, }); const result = await this.prisma.monitoringStateRecord.upsert({ where: { userId }, update: { currentState: newState, reason, lastUpdatedAt: new Date() }, create: { userId, currentState: newState, reason, lastUpdatedAt: new Date() }, }); // Fire external notifications only when state actually escalates const escalationOrder = ['NORMAL', 'MONITOR', 'URGENT', 'EMERGENCY']; const previousLevel = escalationOrder.indexOf(previous?.currentState ?? 'NORMAL'); const newLevel = escalationOrder.indexOf(newState); if (newLevel > previousLevel) { // State escalated — fire emergency service await this.emergencyService.handleStateTransition(userId, newState, reason); } return result; } private async createFollowUpTask( userId: string, type: FollowUpTaskType, dueAt: Date, relatedReadingId?: string, ) { // Check for existing pending tasks of the same type to avoid duplicates const existingTask = await this.prisma.followUpTask.findFirst({ where: { userId, type, status: FollowUpTaskStatus.PENDING, }, }); if (existingTask) { // Update existing task if dueAt is earlier, or if it's a RECHECK and relatedReadingId is different if ( existingTask.dueAt > dueAt || (type === FollowUpTaskType.RECHECK && existingTask.relatedReadingId !== relatedReadingId) ) { return this.prisma.followUpTask.update({ where: { id: existingTask.id }, data: { dueAt, relatedReadingId }, }); } return existingTask; // Return existing task if no update is needed } return this.prisma.followUpTask.create({ data: { userId, type, dueAt, relatedReadingId, }, }); } }