use axum::{ extract::{Extension, Json, Path, Query, State}, http::StatusCode, }; use std::time::SystemTime; use crate::{ auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth config::AppState, models::medication::{ CreateMedicationRequest, LogDoseRequest, Medication, MedicationDose, MedicationRepository, MedicationResponse, UpdateMedicationRequest, }, }; #[derive(serde::Deserialize)] pub struct ListMedicationsQuery { pub profile_id: Option, pub active: Option, pub limit: Option, } pub async fn create_medication( State(state): State, Extension(claims): Extension, Json(req): Json, ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); let now = SystemTime::now(); let medication_id = uuid::Uuid::new_v4().to_string(); // Zero-knowledge: the client sends an already-encrypted blob; the server // stores it verbatim and never inspects the contents. let medication_data = crate::models::health_data::EncryptedField { data: req.encrypted_data.data, encrypted: true, iv: req.encrypted_data.iv, auth_tag: req.encrypted_data.auth_tag, }; let medication = Medication { id: None, medication_id, user_id: claims.sub, profile_id: req.profile_id.clone(), medication_data, reminders: vec![], created_at: now.into(), updated_at: now.into(), active: req.active, dose_schedule: req.dose_schedule, pill_identification: None, }; match repo.create(medication).await { Ok(med) => Ok(Json(med.into())), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } pub async fn list_medications( State(state): State, Extension(claims): Extension, Query(query): Query, ) -> Result>, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); // Honor the active filter (and optional profile_id) when provided. match repo .find_by_user_filtered(&claims.sub, query.active, query.profile_id.as_deref()) .await { Ok(medications) => { let resp: Vec = medications.into_iter().map(Into::into).collect(); Ok(Json(resp)) } Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } pub async fn get_medication( State(state): State, Extension(_claims): Extension, Path(id): Path, ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); // The path param is the application-level medication_id (a UUID), not the // Mongo _id, so look it up directly instead of parsing as ObjectId. match repo.find_by_medication_id(&id).await { Ok(Some(medication)) => Ok(Json(medication.into())), Ok(None) => Err(StatusCode::NOT_FOUND), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } pub async fn update_medication( State(state): State, Extension(_claims): Extension, Path(id): Path, Json(req): Json, ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); // Look up by medication_id (UUID) — the path param is not a Mongo ObjectId. match repo.update_by_medication_id(&id, req).await { Ok(Some(medication)) => Ok(Json(medication.into())), Ok(None) => Err(StatusCode::NOT_FOUND), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } pub async fn delete_medication( State(state): State, Extension(_claims): Extension, Path(id): Path, ) -> Result { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); // Look up by medication_id (UUID), not Mongo _id. match repo.delete_by_medication_id(&id).await { Ok(true) => Ok(StatusCode::NO_CONTENT), Ok(false) => Err(StatusCode::NOT_FOUND), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } pub async fn log_dose( State(state): State, Extension(claims): Extension, Path(id): Path, Json(req): Json, ) -> Result<(StatusCode, Json), StatusCode> { let database = state.db.get_database(); let now = SystemTime::now(); let mut dose = crate::models::medication::MedicationDose { id: None, medication_id: id.clone(), user_id: claims.sub.clone(), logged_at: now.into(), scheduled_time: req.scheduled_time, taken: req.taken.unwrap_or(true), notes: req.notes, }; let result = database .collection::("medication_doses") .insert_one(&dose, None) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Populate the generated _id so the caller gets the persisted dose back. dose.id = result.inserted_id.as_object_id(); Ok((StatusCode::CREATED, Json(dose))) } pub async fn get_adherence( State(state): State, Extension(_claims): Extension, Path(id): Path, ) -> Result, StatusCode> { use mongodb::bson::{doc, DateTime}; const PERIOD_DAYS: i64 = 30; let database = state.db.get_database(); let doses: mongodb::Collection = database.collection("medication_doses"); let med_repo = MedicationRepository::new(database.collection("medications")); // Look at doses logged in the last PERIOD_DAYS days for this medication. let since = DateTime::from_system_time( SystemTime::now() - std::time::Duration::from_secs(PERIOD_DAYS as u64 * 86400), ); let filter = doc! { "medicationId": &id, "loggedAt": { "$gte": since } }; let total_logged = doses .count_documents(filter.clone(), None) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let taken_filter = doc! { "$and": [filter, doc! { "taken": true }] }; let taken = doses .count_documents(taken_filter, None) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Fetch the medication to check for a dose schedule. let medication = med_repo .find_by_medication_id(&id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let (scheduled_doses, total_doses, missed_doses, rate) = match medication.and_then(|m| m.dose_schedule) { Some(schedule) => { // Compute the expected number of doses in the period from the schedule. let scheduled = compute_scheduled_doses(&schedule, PERIOD_DAYS); let missed = scheduled.saturating_sub(taken as i64); let r = if scheduled == 0 { 0.0 } else { (taken as f64 / scheduled as f64) * 100.0 }; (scheduled, scheduled, missed, r) } None => { // No schedule: fall back to taken / total_logged. let missed = (total_logged as i64).saturating_sub(taken as i64); let r = if total_logged == 0 { 0.0 } else { (taken as f64 / total_logged as f64) * 100.0 }; (total_logged as i64, total_logged as i64, missed, r) } }; Ok(Json(crate::models::medication::AdherenceStats { medication_id: id, total_doses, scheduled_doses, taken_doses: taken as i64, missed_doses, adherence_rate: rate, period_days: PERIOD_DAYS, })) } /// Compute the expected number of doses over the last `days` days from a schedule. fn compute_scheduled_doses(schedule: &crate::models::medication::DoseSchedule, days: i64) -> i64 { use chrono::{Datelike, Utc}; let now = Utc::now(); let weekdays: &[&str] = &["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; // chrono weekday(): 0=Sunday, 1=Monday, ... 6=Saturday let active_days: Vec = if schedule.days_of_week.is_empty() { // Every day. (0..7).collect() } else { schedule .days_of_week .iter() .filter_map(|d| weekdays.iter().position(|&w| w == d)) .map(|pos| { // weekdays is Mon-first (0=Mon); chrono is Sun-first (0=Sun). // Map: Mon=0 -> 1, Tue=1 -> 2, ..., Sun=6 -> 0 if pos == 6 { 0 } else { pos + 1 } }) .collect() }; let mut count: i64 = 0; for i in 0..days { let date = now - chrono::Duration::days(i); let chrono_dow = date.weekday().num_days_from_sunday() as usize; // 0=Sun if active_days.contains(&chrono_dow) { count += schedule.times_per_day as i64; } } count }