//! Facial landmark detection step.
//!
//! Uses dlib to detect 68 facial landmarks for alignment and eye filtering.

use crate::config::Config;
use crate::models::DlibLandmarks;
use crate::pipeline::{
    computed_keys, ComputedValue, Landmarks, PipelineContext, ProcessingStep, StepOutcome,
};
use async_trait::async_trait;
use tokio::task;

/// Detects facial landmarks using dlib.
///
/// This step:
/// 1. Uses dlib to detect faces and 68 landmarks
/// 2. Stores Landmarks in ctx.computed["landmarks"]
pub struct LandmarksStep;

#[async_trait]
impl ProcessingStep for LandmarksStep {
    fn id(&self) -> &'static str {
        "landmarks"
    }

    fn name(&self) -> &'static str {
        "Landmarks"
    }

    async fn execute(&self, mut ctx: PipelineContext, _config: &Config) -> StepOutcome {
        // Landmarks are always needed for face alignment
        let image = match ctx.require_image("landmark detection") {
            Ok(img) => img,
            Err(e) => return StepOutcome::Error { ctx, error: e },
        };

        // Get the global landmark predictor (loaded once, reused for all images)
        let dlib = match DlibLandmarks::global() {
            Ok(d) => d,
            Err(e) => {
                // If model isn't available, skip this step with a warning
                tracing::warn!("Dlib landmarks model not available: {}", e);
                return StepOutcome::Skip {
                    ctx,
                    reason: "landmarks_failed".to_string(),
                    detail: Some(e.to_string()),
                };
            }
        };

        // Convert to RGB for dlib
        let rgb = image.to_rgb8();
        let (width, height) = (rgb.width() as usize, rgb.height() as usize);
        let pixels = rgb.into_raw();

        // Get the face rectangle if available
        let face_rect: Option<(i64, i64, i64, i64)> = ctx
            .get_computed(computed_keys::FACE_RECT)
            .and_then(|v| v.as_face_rect())
            .map(|r| (r.x1 as i64, r.y1 as i64, r.x2 as i64, r.y2 as i64));

        // Run dlib operations in a blocking thread to avoid dropping in async context
        let landmarks_result = task::spawn_blocking(move || -> Result<Landmarks, String> {
            dlib.detect_landmarks(width, height, &pixels, face_rect)
                .map_err(|e| e.to_string())
        })
        .await;

        let landmarks = match landmarks_result {
            Ok(Ok(l)) => l,
            Ok(Err(e)) => {
                return StepOutcome::Skip {
                    ctx,
                    reason: "landmarks_failed".to_string(),
                    detail: Some(e),
                };
            }
            Err(e) => {
                return StepOutcome::Error {
                    ctx,
                    error: format!("Landmark detection task failed: {}", e),
                };
            }
        };

        // Store landmarks
        ctx.set_computed(
            computed_keys::LANDMARKS,
            ComputedValue::Landmarks(Box::new(landmarks)),
        );

        tracing::trace!("Landmarks detected successfully");

        StepOutcome::Continue(ctx)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::immich_api::FaceData;

    fn make_test_ctx() -> PipelineContext {
        let face_data = FaceData {
            bounding_box_x1: 0.0,
            bounding_box_y1: 0.0,
            bounding_box_x2: 100.0,
            bounding_box_y2: 100.0,
            image_width: 100,
            image_height: 100,
        };
        PipelineContext::new("test".to_string(), "2024-01-01".to_string(), face_data)
    }

    #[tokio::test]
    async fn test_no_image_error() {
        let step = LandmarksStep;
        let ctx = make_test_ctx();
        let config = Config::default();

        match step.execute(ctx, &config).await {
            StepOutcome::Error { error, .. } => {
                assert!(error.contains("No image"));
            }
            other => panic!("Expected Error, got {:?}", other),
        }
    }
}
