use serde::Serialize;
use std::sync::{Arc, Mutex, OnceLock};
use std::cmp::Ordering;
use tauri::Emitter;

static MODEL: OnceLock<Arc<Mutex<fastembed::TextEmbedding>>> = OnceLock::new();

/// Compute a single embedding vector for the given text.
#[tauri::command]
pub async fn compute_embedding(
    app: tauri::AppHandle,
    text: String,
) -> Result<Vec<f32>, String> {
    // Lazy init via OnceLock — guarantees single initialization, lock-free reads
    let model = match MODEL.get() {
        Some(m) => m.clone(),
        None => {
            let _ = app.emit("model-loading", serde_json::json!({"status": "loading"}));
            let m = initialize_model().await?;
            let _ = app.emit("model-ready", serde_json::json!({"status": "ready"}));
            let m = Arc::new(Mutex::new(m));
            MODEL.set(m.clone()).map_err(|_| "model already initialized".to_string())?;
            m
        }
    };

    let result = tokio::task::spawn_blocking(move || -> Result<Vec<f32>, String> {
        let mut model = model.lock().map_err(|e| e.to_string())?;
        let embeddings = model.embed(vec![text], None).map_err(|e| e.to_string())?;
        embeddings
            .into_iter()
            .next()
            .ok_or_else(|| "no embedding returned".to_string())
    })
    .await
    .map_err(|e| e.to_string())?
    .map_err(|e| e.to_string())?;

    Ok(result)
}

/// Rank candidates by cosine similarity against a query vector.
#[tauri::command]
pub fn rank_by_similarity(
    query: Vec<f32>,
    candidates: Vec<(String, Vec<f32>)>,
) -> Vec<RankedCandidate> {
    let mut scored: Vec<RankedCandidate> = candidates
        .into_iter()
        .map(|(id, emb)| {
            let score = cosine_similarity(&query, &emb);
            RankedCandidate { block_id: id, score }
        })
        .collect();
    scored.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(Ordering::Equal)
    });
    scored
}

#[derive(Debug, Serialize)]
pub struct RankedCandidate {
    pub block_id: String,
    pub score: f32,
}

pub(crate) fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
    // Guard against dimension mismatch — silent truncation with zip() is dangerous
    if a.len() != b.len() || a.is_empty() {
        return 0.0;
    }

    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
    if norm_a == 0.0 || norm_b == 0.0 {
        0.0
    } else {
        dot / (norm_a * norm_b)
    }
}

async fn initialize_model() -> Result<fastembed::TextEmbedding, String> {
    tokio::task::spawn_blocking(|| {
        fastembed::TextEmbedding::try_new(
            fastembed::TextInitOptions::new(fastembed::EmbeddingModel::AllMiniLML6V2),
        )
        .map_err(|e| format!("failed to init embedding model: {}", e))
    })
    .await
    .map_err(|e| e.to_string())?
    .map_err(|e| e.to_string())
}

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

    // ── cosine_similarity unit tests ───────────────────────────

    #[test]
    fn identical_vectors_score_one() {
        let v = vec![1.0, 2.0, 3.0, 4.0];
        let score = cosine_similarity(&v, &v);
        assert!((score - 1.0).abs() < 1e-6, "expected 1.0, got {}", score);
    }

    #[test]
    fn orthogonal_vectors_score_zero() {
        let score = cosine_similarity(&[1.0, 0.0], &[0.0, 1.0]);
        assert!((score - 0.0).abs() < 1e-6, "expected 0.0, got {}", score);
    }

    #[test]
    fn opposite_vectors_score_negative_one() {
        let score = cosine_similarity(&[1.0, 0.0], &[-1.0, 0.0]);
        assert!((score - (-1.0)).abs() < 1e-6, "expected -1.0, got {}", score);
    }

    #[test]
    fn zero_query_vector_returns_zero() {
        let score = cosine_similarity(&[0.0, 0.0, 0.0], &[1.0, 2.0, 3.0]);
        assert_eq!(score, 0.0);
    }

    #[test]
    fn zero_candidate_vector_returns_zero() {
        let score = cosine_similarity(&[1.0, 2.0, 3.0], &[0.0, 0.0, 0.0]);
        assert_eq!(score, 0.0);
    }

    #[test]
    fn parallel_vectors_score_one() {
        let v1 = vec![2.0, 4.0, 6.0];
        let v2 = vec![1.0, 2.0, 3.0];
        let score = cosine_similarity(&v1, &v2);
        assert!((score - 1.0).abs() < 1e-6, "expected 1.0, got {}", score);
    }

    #[test]
    fn mismatched_dimensions_returns_zero() {
        let score = cosine_similarity(&[1.0, 0.0], &[0.0, 1.0, 5.0]);
        assert_eq!(score, 0.0, "mismatched dims should return 0.0");
    }

    #[test]
    fn empty_vectors_returns_zero() {
        let score = cosine_similarity(&[], &[]);
        assert_eq!(score, 0.0);
    }

    // ── rank_by_similarity integration tests ───────────────────

    #[test]
    fn ranks_highest_similarity_first() {
        let query = vec![1.0, 0.0, 0.0];
        let candidates = vec![
            ("far".into(), vec![0.0, 1.0, 0.0]),
            ("close".into(), vec![0.9, 0.0, 0.1]),
            ("middle".into(), vec![0.5, 0.5, 0.0]),
        ];

        let result = rank_by_similarity(query, candidates);
        assert_eq!(result.len(), 3);
        assert_eq!(result[0].block_id, "close");
        assert_eq!(result[1].block_id, "middle");
        assert_eq!(result[2].block_id, "far");
    }

    #[test]
    fn empty_candidates_returns_empty() {
        let result = rank_by_similarity(vec![1.0, 0.0], vec![]);
        assert!(result.is_empty());
    }

    #[test]
    fn single_candidate_scores_one() {
        let result = rank_by_similarity(
            vec![1.0, 0.0],
            vec![("only".into(), vec![1.0, 0.0])],
        );
        assert_eq!(result.len(), 1);
        assert!((result[0].score - 1.0).abs() < 1e-6);
    }

    // ── RankedCandidate serialization ──────────────────────────

    #[test]
    fn ranked_candidate_serializes() {
        let c = RankedCandidate {
            block_id: "abc".into(),
            score: 0.75,
        };
        let json = serde_json::to_string(&c).unwrap();
        assert!(json.contains(r#""block_id":"abc""#));
        assert!(json.contains(r#""score":0.75"#));
    }
}
