1use axum::{
2 extract::{Path, State},
3 http::StatusCode,
4 routing::{get, post},
5 Json, Router,
6};
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use crate::db::Database;
11use crate::models::{Sighting, Source};
12use crate::streaming::{Broadcast, StreamingEvent};
13use crate::websocket::ws_handler;
14
15#[derive(Clone)]
16pub struct AppState {
17 pub db: Arc<Database>,
18 pub broadcast: Broadcast,
19}
20
21#[derive(Deserialize)]
22pub struct CreateSighting {
23 pub species: String,
24 pub scientific_name: Option<String>,
25 pub latitude: f64,
26 pub longitude: f64,
27 pub source: String,
28 pub source_id: String,
29 pub details: Option<String>,
30}
31
32#[derive(Serialize)]
33pub struct ApiResponse<T> {
34 pub success: bool,
35 pub data: Option<T>,
36 pub error: Option<String>,
37}
38
39pub fn build_router(state: AppState) -> Router {
40 Router::new()
41 .route("/api/sightings", get(get_sightings).post(create_sighting))
42 .route(
43 "/api/sightings/{id}",
44 get(get_sighting_by_id).put(update_sighting),
45 )
46 .route("/api/analysis/{id}", post(run_analysis))
47 .route("/api/export/{format}", get(export_sightings))
48 .route("/ws/stream", get(ws_handler))
49 .with_state(state)
50}
51
52pub async fn start_server(host: &str, port: u16, state: AppState) -> anyhow::Result<()> {
53 let router = build_router(state);
54 let addr = format!("{}:{}", host, port);
55 let listener = tokio::net::TcpListener::bind(&addr).await?;
56 axum::serve(listener, router).await?;
57 Ok(())
58}
59
60async fn get_sightings(
61 State(state): State<AppState>,
62) -> Result<Json<ApiResponse<Vec<Sighting>>>, (StatusCode, Json<ApiResponse<()>>)> {
63 match state.db.get_all_sightings() {
64 Ok(sightings) => Ok(Json(ApiResponse {
65 success: true,
66 data: Some(sightings),
67 error: None,
68 })),
69 Err(e) => Err((
70 StatusCode::INTERNAL_SERVER_ERROR,
71 Json(ApiResponse {
72 success: false,
73 data: None,
74 error: Some(e.to_string()),
75 }),
76 )),
77 }
78}
79
80async fn get_sighting_by_id(
81 State(state): State<AppState>,
82 Path(id): Path<i64>,
83) -> Result<Json<ApiResponse<Sighting>>, (StatusCode, Json<ApiResponse<()>>)> {
84 let sightings = state.db.get_all_sightings().map_err(|e| {
85 (
86 StatusCode::INTERNAL_SERVER_ERROR,
87 Json(ApiResponse {
88 success: false,
89 data: None,
90 error: Some(e.to_string()),
91 }),
92 )
93 })?;
94
95 match sightings.into_iter().find(|s| s.id == Some(id)) {
96 Some(sighting) => Ok(Json(ApiResponse {
97 success: true,
98 data: Some(sighting),
99 error: None,
100 })),
101 None => Err((
102 StatusCode::NOT_FOUND,
103 Json(ApiResponse {
104 success: false,
105 data: None,
106 error: Some("Sighting not found".to_string()),
107 }),
108 )),
109 }
110}
111
112async fn create_sighting(
113 State(state): State<AppState>,
114 Json(input): Json<CreateSighting>,
115) -> Result<(StatusCode, Json<ApiResponse<i64>>), (StatusCode, Json<ApiResponse<()>>)> {
116 let now = chrono::Utc::now();
117 let source = match input.source.as_str() {
118 "GBIF" => Source::GBIF,
119 "Movebank" => Source::Movebank,
120 "iNaturalist" | "INaturalist" => Source::INaturalist,
121 "IUCN" => Source::IUCN,
122 _ => Source::GBIF,
123 };
124
125 let sighting = Sighting {
126 id: None,
127 species: input.species,
128 scientific_name: input.scientific_name,
129 latitude: input.latitude,
130 longitude: input.longitude,
131 observed_on: now,
132 source,
133 source_id: input.source_id,
134 details: input.details,
135 };
136
137 match state.db.insert_sighting(&sighting) {
138 Ok(id) => {
139 let mut saved = sighting.clone();
140 saved.id = Some(id);
141 let _ = state
142 .broadcast
143 .publish(StreamingEvent::SightingCreated(saved));
144 Ok((
145 StatusCode::CREATED,
146 Json(ApiResponse {
147 success: true,
148 data: Some(id),
149 error: None,
150 }),
151 ))
152 }
153 Err(e) => Err((
154 StatusCode::INTERNAL_SERVER_ERROR,
155 Json(ApiResponse {
156 success: false,
157 data: None,
158 error: Some(e.to_string()),
159 }),
160 )),
161 }
162}
163
164async fn update_sighting(
165 State(state): State<AppState>,
166 Path(id): Path<i64>,
167 Json(input): Json<CreateSighting>,
168) -> Result<Json<ApiResponse<i64>>, (StatusCode, Json<ApiResponse<()>>)> {
169 let now = chrono::Utc::now();
170 let source = match input.source.as_str() {
171 "GBIF" => Source::GBIF,
172 "Movebank" => Source::Movebank,
173 "iNaturalist" | "INaturalist" => Source::INaturalist,
174 "IUCN" => Source::IUCN,
175 _ => Source::GBIF,
176 };
177
178 let sighting = Sighting {
179 id: Some(id),
180 species: input.species,
181 scientific_name: input.scientific_name,
182 latitude: input.latitude,
183 longitude: input.longitude,
184 observed_on: now,
185 source,
186 source_id: input.source_id,
187 details: input.details,
188 };
189
190 match state.db.insert_sighting(&sighting) {
191 Ok(_inserted_id) => {
192 let _ = state
193 .broadcast
194 .publish(StreamingEvent::SightingUpdated(sighting));
195 Ok(Json(ApiResponse {
196 success: true,
197 data: Some(id),
198 error: None,
199 }))
200 }
201 Err(e) => Err((
202 StatusCode::INTERNAL_SERVER_ERROR,
203 Json(ApiResponse {
204 success: false,
205 data: None,
206 error: Some(e.to_string()),
207 }),
208 )),
209 }
210}
211
212async fn run_analysis(
213 State(state): State<AppState>,
214 Path(id): Path<i64>,
215) -> Result<Json<ApiResponse<String>>, (StatusCode, Json<ApiResponse<()>>)> {
216 let sightings = state.db.get_all_sightings().map_err(|e| {
217 (
218 StatusCode::INTERNAL_SERVER_ERROR,
219 Json(ApiResponse {
220 success: false,
221 data: None,
222 error: Some(e.to_string()),
223 }),
224 )
225 })?;
226
227 let analysis_id = format!("analysis_{}", id);
228 let msg = format!(
229 "Analysis complete for sighting {}: {} total sightings in dataset",
230 id,
231 sightings.len()
232 );
233 let _ = state
234 .broadcast
235 .publish(StreamingEvent::AnalysisComplete(msg.clone()));
236
237 Ok(Json(ApiResponse {
238 success: true,
239 data: Some(analysis_id),
240 error: None,
241 }))
242}
243
244async fn export_sightings(
245 State(state): State<AppState>,
246 Path(format): Path<String>,
247) -> Result<Json<ApiResponse<serde_json::Value>>, (StatusCode, Json<ApiResponse<()>>)> {
248 let sightings = state.db.get_all_sightings().map_err(|e| {
249 (
250 StatusCode::INTERNAL_SERVER_ERROR,
251 Json(ApiResponse {
252 success: false,
253 data: None,
254 error: Some(e.to_string()),
255 }),
256 )
257 })?;
258
259 match format.as_str() {
260 "json" => {
261 let json_data = serde_json::to_value(&sightings).map_err(|e| {
262 (
263 StatusCode::INTERNAL_SERVER_ERROR,
264 Json(ApiResponse {
265 success: false,
266 data: None,
267 error: Some(e.to_string()),
268 }),
269 )
270 })?;
271 Ok(Json(ApiResponse {
272 success: true,
273 data: Some(json_data),
274 error: None,
275 }))
276 }
277 _ => Err((
278 StatusCode::BAD_REQUEST,
279 Json(ApiResponse {
280 success: false,
281 data: None,
282 error: Some(format!("Unsupported export format: {}", format)),
283 }),
284 )),
285 }
286}