worker-service 0.2.0

Worker Service - A worker administration microservice that interoperates with the worker-matcher crate
//! 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::Worker;
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: "worker-service".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    })
}

/// Create worker request
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateWorkerRequest {
    #[serde(flatten)]
    pub worker: Worker,
}

/// Create a new worker
pub async fn create_worker(
    State(_state): State<AppState>,
    Json(payload): Json<Worker>,
) -> impl IntoResponse {
    // TODO: Actually insert into database using Diesel
    // For now, return the worker as-is
    // In a real implementation:
    // 1. Validate worker 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 worker by ID
#[utoipa::path(
    get,
    path = "/api/v1/workers/{id}",
    tag = "workers",
    params(
        ("id" = Uuid, Path, description = "Worker UUID")
    ),
    responses(
        (status = 200, description = "Worker found", body = ApiResponse<Worker>),
        (status = 404, description = "Worker not found", body = ApiResponse<()>),
        (status = 500, description = "Internal server error", body = ApiResponse<()>)
    )
)]
pub async fn get_worker(
    State(_state): State<AppState>,
    Path(_id): Path<Uuid>,
) -> impl IntoResponse {
    // TODO: Implement worker retrieval from database
    // 1. Query database by UUID
    // 2. Convert DbWorker to Worker model
    // 3. Return worker or 404

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

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

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

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

    (StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
        "NOT_IMPLEMENTED",
        "Worker 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 workers: Vec<Worker>,
    pub total: usize,
    pub query: String,
}

/// Search for workers
#[utoipa::path(
    get,
    path = "/api/v1/workers/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_workers(
    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 worker_ids = if params.fuzzy {
        state.search_engine.fuzzy_search(&params.q, limit)
    } else {
        state.search_engine.search(&params.q, limit)
    };

    match worker_ids {
        Ok(ids) => {
            // TODO: Fetch full worker records from database
            // For now, return empty list
            let response = SearchResponse {
                workers: 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 {
    /// Worker to match against existing records
    #[serde(flatten)]
    pub worker: Worker,

    /// 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 worker: Worker,
    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 worker against existing records
#[utoipa::path(
    post,
    path = "/api/v1/workers/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_worker(
    State(state): State<AppState>,
    Json(payload): Json<MatchRequest>,
) -> impl IntoResponse {
    // Use search engine to get candidate workers (blocking)
    let family_name = &payload.worker.name.family;
    let birth_year = payload.worker.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 worker 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))
        }
    }
}