using ClinicalInsightsPro.API.Data; using ClinicalInsightsPro.API.DTOs; using ClinicalInsightsPro.API.Models; using ClinicalInsightsPro.API.Services.Interfaces; using Microsoft.EntityFrameworkCore; namespace ClinicalInsightsPro.API.Services; // Questionnaire-based rule engine that scores an encounter as High or Normal // risk, combining chart data (conditions, vitals) with clinician-entered // questionnaire responses. Rule weights are intentionally simple and modular // so they can be extended, or eventually replaced by an ML model, per scope. public class RiskScoringService : IRiskScoringService { private readonly ApplicationDbContext _db; private const int HighRiskThreshold = 50; public RiskScoringService(ApplicationDbContext db) { _db = db; } public async Task AssessAsync(string patientId, QuestionnaireDto questionnaire) { var patient = await _db.Patients .Include(p => p.Conditions) .Include(p => p.Observations) .Include(p => p.Allergies) .Include(p => p.Medications) .Include(p => p.Encounters) .FirstOrDefaultAsync(p => p.Id == patientId); if (patient == null) throw new KeyNotFoundException($"Patient '{patientId}' was not found."); int score = 0; var factors = new List(); foreach (var c in patient.Conditions.Where(c => c.ClinicalStatus == "active")) { switch (c.Code) { case "E11.9": score += 20; factors.Add("Active Type 2 diabetes (+20)"); break; case "I10": score += 15; factors.Add("Essential hypertension (+15)"); break; case "J45.909": score += 10; factors.Add("Asthma (+10)"); break; default: score += 5; factors.Add($"Active condition: {c.Display} (+5)"); break; } } var latestSystolic = patient.Observations .Where(o => o.Code == "8480-6") .OrderByDescending(o => o.EffectiveDate) .FirstOrDefault(); if (latestSystolic != null && latestSystolic.Value >= 160) { score += 20; factors.Add($"Systolic BP {latestSystolic.Value} mmHg — hypertensive urgency range (+20)"); } else if (latestSystolic != null && latestSystolic.Value >= 140) { score += 10; factors.Add($"Systolic BP {latestSystolic.Value} mmHg — stage 2 hypertension (+10)"); } var latestGlucose = patient.Observations .Where(o => o.Code == "2339-0") .OrderByDescending(o => o.EffectiveDate) .FirstOrDefault(); if (latestGlucose != null && latestGlucose.Value >= 200) { score += 15; factors.Add($"Glucose {latestGlucose.Value} mg/dL — poorly controlled (+15)"); } // Allergies — a known high-criticality allergy raises the risk of a // future adverse drug event, independent of what's happening today. var highCriticalityAllergies = patient.Allergies.Count(a => a.Criticality == "high"); if (highCriticalityAllergies > 0) { var allergyPoints = Math.Min(highCriticalityAllergies * 10, 20); score += allergyPoints; factors.Add($"{highCriticalityAllergies} high-criticality allerg{(highCriticalityAllergies == 1 ? "y" : "ies")} on file (+{allergyPoints})"); } // Medications — polypharmacy (commonly defined as 5+ concurrent // medications) is itself an established risk factor for interactions // and adverse events, regardless of what the medications are for. var activeMedicationCount = patient.Medications.Count(m => m.Status == "active"); if (activeMedicationCount >= 5) { score += 12; factors.Add($"Polypharmacy — {activeMedicationCount} active medications (+12)"); } else if (activeMedicationCount >= 3) { score += 6; factors.Add($"{activeMedicationCount} active medications (+6)"); } // Encounters — frequent recent encounters (visits, admissions, ED // visits) in the last 12 months suggest an unstable or worsening // condition even if no single encounter looks alarming on its own. var recentEncounterCount = patient.Encounters.Count(e => e.EncounterDate >= DateTime.UtcNow.AddMonths(-12)); if (recentEncounterCount >= 3) { score += 12; factors.Add($"{recentEncounterCount} encounters within the last 12 months (+12)"); } else if (recentEncounterCount == 2) { score += 6; factors.Add("2 encounters within the last 12 months (+6)"); } if (questionnaire.Smoker) { score += 10; factors.Add("Current smoker (+10)"); } if (questionnaire.FamilyHistoryHeartDisease) { score += 8; factors.Add("Family history of heart disease (+8)"); } if (questionnaire.PriorHospitalization) { score += 12; factors.Add("Prior hospitalization within 12 months (+12)"); } if (questionnaire.Bmi is >= 30) { score += 8; factors.Add($"BMI {questionnaire.Bmi:F1} — obese range (+8)"); } if (questionnaire.PainScore >= 7) { score += 5; factors.Add($"Reported pain score {questionnaire.PainScore}/10 (+5)"); } score = Math.Min(score, 100); var level = score >= HighRiskThreshold ? "High" : "Normal"; if (!factors.Any()) factors.Add("No significant risk factors identified from chart or questionnaire."); var assessment = new RiskAssessment { PatientId = patientId, Score = score, RiskLevel = level, Rationale = string.Join("; ", factors), AssessedDate = DateTime.UtcNow }; _db.RiskAssessments.Add(assessment); await _db.SaveChangesAsync(); return new RiskAssessmentResultDto { PatientId = patientId, Score = score, RiskLevel = level, ContributingFactors = factors, AssessedDate = assessment.AssessedDate }; } }