use axum::{ Json, extract::{Path, State}, http::StatusCode, response::IntoResponse, }; use chrono::Utc; use uuid::Uuid; use domain::{self, DomainError}; use crate::{ dto::{ScheduleHistoryEntry, ScheduleResponse}, error::ApiError, extractors::CurrentUser, state::AppState, }; use super::require_owner; /// Trigger 48-hour schedule generation for a channel, starting from now. /// Replaces any existing schedule for the same window. pub(super) async fn generate_schedule( State(state): State, CurrentUser(user): CurrentUser, Path(channel_id): Path, ) -> Result { let channel = state.channel_service.find_by_id(channel_id).await?; require_owner(&channel, user.id)?; let schedule = state .schedule_engine .generate_schedule(channel_id, Utc::now()) .await?; let _ = state.event_tx.send(domain::DomainEvent::ScheduleGenerated { channel_id, schedule: schedule.clone(), }); let detail = format!("{} slots", schedule.slots.len()); let _ = state.activity_log_repo.log("schedule_generated", &detail, Some(channel_id)).await; Ok((StatusCode::CREATED, Json(ScheduleResponse::from(schedule)))) } /// Return the currently active 48-hour schedule for a channel. /// 404 if no schedule has been generated yet — call POST /:id/schedule first. pub(super) async fn get_active_schedule( State(state): State, CurrentUser(user): CurrentUser, Path(channel_id): Path, ) -> Result { let channel = state.channel_service.find_by_id(channel_id).await?; require_owner(&channel, user.id)?; let schedule = state .schedule_engine .get_active_schedule(channel_id, Utc::now()) .await? .ok_or(DomainError::NoActiveSchedule(channel_id))?; Ok(Json(ScheduleResponse::from(schedule))) } /// List all schedule generations for a channel, newest first. /// Returns lightweight entries (no slots). pub(super) async fn list_schedule_history( State(state): State, CurrentUser(user): CurrentUser, Path(channel_id): Path, ) -> Result { let channel = state.channel_service.find_by_id(channel_id).await?; require_owner(&channel, user.id)?; let history = state.schedule_engine.list_schedule_history(channel_id).await?; let entries: Vec = history.into_iter().map(Into::into).collect(); Ok(Json(entries)) } /// Fetch a single historical schedule with all its slots. pub(super) async fn get_schedule_history_entry( State(state): State, CurrentUser(user): CurrentUser, Path((channel_id, gen_id)): Path<(Uuid, Uuid)>, ) -> Result { let channel = state.channel_service.find_by_id(channel_id).await?; require_owner(&channel, user.id)?; let schedule = state .schedule_engine .get_schedule_by_id(channel_id, gen_id) .await? .ok_or_else(|| ApiError::NotFound(format!("Schedule {} not found", gen_id)))?; Ok(Json(ScheduleResponse::from(schedule))) } /// Roll back to a previous schedule generation. /// /// Deletes all generations after `gen_id`'s generation, then generates a fresh /// schedule from now (inheriting the rolled-back generation as the base for /// recycle-policy history). pub(super) async fn rollback_schedule( State(state): State, CurrentUser(user): CurrentUser, Path((channel_id, gen_id)): Path<(Uuid, Uuid)>, ) -> Result { let channel = state.channel_service.find_by_id(channel_id).await?; require_owner(&channel, user.id)?; let target = state .schedule_engine .get_schedule_by_id(channel_id, gen_id) .await? .ok_or_else(|| ApiError::NotFound(format!("Schedule {} not found", gen_id)))?; state .schedule_engine .delete_schedules_after(channel_id, target.generation) .await?; let schedule = state .schedule_engine .generate_schedule(channel_id, Utc::now()) .await?; let _ = state.event_tx.send(domain::DomainEvent::ScheduleGenerated { channel_id, schedule: schedule.clone(), }); let detail = format!("rollback to gen {}; {} slots", target.generation, schedule.slots.len()); let _ = state.activity_log_repo.log("schedule_rollback", &detail, Some(channel_id)).await; Ok(Json(ScheduleResponse::from(schedule))) }