person-service 0.2.0

Person Service (MPI) - A healthcare person identification and matching system
//! REST API request handlers

use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    Json,
    response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use utoipa::ToSchema;

use crate::models::Patient;
use crate::api::{ApiResponse, ApiError};
use crate::matching::MatchResult;
use super::state::AppState;

/// Health check response
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct HealthResponse {
    pub status: String,
    pub service: String,
    pub version: String,
}

/// Health check endpoint
#[utoipa::path(
    get,
    path = "/api/v1/health",
    tag = "health",
    responses(
        (status = 200, description = "Service is healthy", body = HealthResponse)
    )
)]
pub async fn health_check() -> impl IntoResponse {
    Json(HealthResponse {
        status: "healthy".to_string(),
        service: "main-patient-index".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    })
}

/// Create patient request
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreatePatientRequest {
    #[serde(flatten)]
    pub patient: Patient,
}

/// Create a new patient
pub async fn create_patient(
    State(_state): State<AppState>,
    Json(payload): Json<Patient>,
) -> impl IntoResponse {
    // TODO: Actually insert into database using Diesel
    // For now, return the patient as-is
    // In a real implementation:
    // 1. Validate patient data
    // 2. Check for duplicates using matcher
    // 3. Insert into database
    // 4. Index in search engine
    // 5. Publish event to stream

    (StatusCode::CREATED, Json(ApiResponse::success(payload)))
}

/// Get a patient by ID
#[utoipa::path(
    get,
    path = "/api/v1/patients/{id}",
    tag = "patients",
    params(
        ("id" = Uuid, Path, description = "Patient UUID")
    ),
    responses(
        (status = 200, description = "Patient found", body = ApiResponse<Patient>),
        (status = 404, description = "Patient not found", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn get_patient(
    State(_state): State<AppState>,
    Path(_id): Path<Uuid>,
) -> impl IntoResponse {
    // TODO: Implement patient retrieval from database
    // 1. Query database by UUID
    // 2. Convert DbPatient to Patient model
    // 3. Return patient or 404

    (StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
        "NOT_IMPLEMENTED",
        "Patient retrieval not yet implemented"
    )))
}

/// Update a patient
#[utoipa::path(
    put,
    path = "/api/v1/patients/{id}",
    tag = "patients",
    params(
        ("id" = Uuid, Path, description = "Patient UUID")
    ),
    request_body = Patient,
    responses(
        (status = 200, description = "Patient updated", body = ApiResponse<Patient>),
        (status = 404, description = "Patient not found", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn update_patient(
    State(_state): State<AppState>,
    Path(_id): Path<Uuid>,
    Json(_payload): Json<Patient>,
) -> impl IntoResponse {
    // TODO: Implement patient update
    // 1. Verify patient exists
    // 2. Update database record
    // 3. Update search index
    // 4. Publish update event

    (StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
        "NOT_IMPLEMENTED",
        "Patient update not yet implemented"
    )))
}

/// Delete a patient (soft delete)
#[utoipa::path(
    delete,
    path = "/api/v1/patients/{id}",
    tag = "patients",
    params(
        ("id" = Uuid, Path, description = "Patient UUID")
    ),
    responses(
        (status = 204, description = "Patient deleted"),
        (status = 404, description = "Patient not found", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn delete_patient(
    State(_state): State<AppState>,
    Path(_id): Path<Uuid>,
) -> impl IntoResponse {
    // TODO: Implement soft patient deletion
    // 1. Set deleted_at timestamp
    // 2. Optionally remove from search index
    // 3. Publish deletion event

    (StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
        "NOT_IMPLEMENTED",
        "Patient deletion not yet implemented"
    )))
}

/// Search query parameters
#[derive(Debug, Deserialize, ToSchema)]
pub struct SearchQuery {
    /// Search query string
    pub q: String,

    /// Maximum number of results (default: 10, max: 100)
    #[serde(default = "default_limit")]
    pub limit: usize,

    /// Use fuzzy search
    #[serde(default)]
    pub fuzzy: bool,
}

fn default_limit() -> usize {
    10
}

/// Search results response
#[derive(Debug, Serialize, ToSchema)]
pub struct SearchResponse {
    pub patients: Vec<Patient>,
    pub total: usize,
    pub query: String,
}

/// Search for patients
#[utoipa::path(
    get,
    path = "/api/v1/patients/search",
    tag = "search",
    params(
        ("q" = String, Query, description = "Search query"),
        ("limit" = Option<usize>, Query, description = "Maximum results (default: 10, max: 100)"),
        ("fuzzy" = Option<bool>, Query, description = "Enable fuzzy search")
    ),
    responses(
        (status = 200, description = "Search results", body = ApiResponse<SearchResponse>),
        (status = 400, description = "Invalid query", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn search_patients(
    State(state): State<AppState>,
    Query(params): Query<SearchQuery>,
) -> impl IntoResponse {
    // Limit to max 100 results
    let limit = params.limit.min(100);

    // Perform search using search engine
    let patient_ids = if params.fuzzy {
        state.search_engine.fuzzy_search(&params.q, limit)
    } else {
        state.search_engine.search(&params.q, limit)
    };

    match patient_ids {
        Ok(ids) => {
            // TODO: Fetch full patient records from database
            // For now, return empty list
            let response = SearchResponse {
                patients: vec![],
                total: ids.len(),
                query: params.q,
            };
            (StatusCode::OK, Json(ApiResponse::success(response)))
        }
        Err(e) => {
            let error = ApiResponse::<SearchResponse>::error(
                "SEARCH_ERROR",
                format!("Search failed: {}", e)
            );
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error))
        }
    }
}

/// Match request payload
#[derive(Debug, Deserialize, ToSchema)]
pub struct MatchRequest {
    /// Patient to match against existing records
    #[serde(flatten)]
    pub patient: Patient,

    /// Minimum match score threshold (0.0 to 1.0)
    #[serde(default)]
    pub threshold: Option<f64>,

    /// Maximum number of matches to return
    #[serde(default = "default_match_limit")]
    pub limit: usize,
}

fn default_match_limit() -> usize {
    10
}

/// Match result with score
#[derive(Debug, Serialize, ToSchema)]
pub struct MatchResponse {
    pub patient: Patient,
    pub score: f64,
    pub quality: String,
}

/// Match results response
#[derive(Debug, Serialize, ToSchema)]
pub struct MatchResultsResponse {
    pub matches: Vec<MatchResponse>,
    pub total: usize,
}

/// Match a patient against existing records
#[utoipa::path(
    post,
    path = "/api/v1/patients/match",
    tag = "matching",
    request_body = MatchRequest,
    responses(
        (status = 200, description = "Matching results", body = ApiResponse<MatchResultsResponse>),
        (status = 400, description = "Invalid request", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn match_patient(
    State(state): State<AppState>,
    Json(payload): Json<MatchRequest>,
) -> impl IntoResponse {
    // Use search engine to get candidate patients (blocking)
    let family_name = &payload.patient.name.family;
    let birth_year = payload.patient.birth_date.map(|d| d.year());

    let candidate_ids = state.search_engine
        .search_by_name_and_year(family_name, birth_year, 100);

    match candidate_ids {
        Ok(ids) => {
            // TODO: Fetch full patient records from database
            // TODO: Run matcher.find_matches() on candidates
            // For now, return empty results
            let response = MatchResultsResponse {
                matches: vec![],
                total: ids.len(),
            };
            (StatusCode::OK, Json(ApiResponse::success(response)))
        }
        Err(e) => {
            let error = ApiResponse::<MatchResultsResponse>::error(
                "MATCH_ERROR",
                format!("Matching failed: {}", e)
            );
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error))
        }
    }
}