Documentation
use std::cmp::Ordering;

use serde_yaml::{Mapping, Value};

use crate::model::Key;
use crate::query::document::{FieldPath, Sort, SortDir};
use crate::query::filter::cmp_ordered;
use crate::query::frontmatter::is_reserved_segment;

pub fn sort_in_place(rows: &mut [(Key, Mapping)], sort: &Sort) {
    rows.sort_by(|a, b| {
        let primary = compare_values(lookup(&a.1, &sort.key), lookup(&b.1, &sort.key), sort.dir);
        primary.then_with(|| a.0.cmp(&b.0))
    });
}

fn lookup<'a>(doc: &'a Mapping, path: &FieldPath) -> Option<&'a Value> {
    let segments = path.segments();
    if segments.is_empty() {
        return None;
    }
    if segments.iter().any(|s| is_reserved_segment(s)) {
        return None;
    }
    let mut current = doc.get(Value::String(segments[0].clone()))?;
    for seg in &segments[1..] {
        current = match current {
            Value::Mapping(m) => m.get(Value::String(seg.clone()))?,
            _ => return None,
        };
    }
    Some(current)
}

fn compare_values(a: Option<&Value>, b: Option<&Value>, dir: SortDir) -> Ordering {
    let a_null = is_null_or_missing(a);
    let b_null = is_null_or_missing(b);
    let raw = match (a_null, b_null) {
        (true, true) => Ordering::Equal,
        (true, false) => Ordering::Less,
        (false, true) => Ordering::Greater,
        (false, false) => total_cmp(a.unwrap(), b.unwrap()),
    };
    match dir {
        SortDir::Asc => raw,
        SortDir::Desc => raw.reverse(),
    }
}

fn total_cmp(a: &Value, b: &Value) -> Ordering {
    if let Some(ord) = cmp_ordered(a, b) {
        return ord;
    }
    let (ra, rb) = (type_rank(a), type_rank(b));
    if ra != rb {
        return ra.cmp(&rb);
    }
    match (a, b) {
        (Value::Number(x), Value::Number(y)) => x
            .as_f64()
            .unwrap_or(f64::NAN)
            .total_cmp(&y.as_f64().unwrap_or(f64::NAN)),
        _ => Ordering::Equal,
    }
}

fn type_rank(v: &Value) -> u8 {
    match v {
        Value::Null => 0,
        Value::Bool(_) => 1,
        Value::Number(_) => 2,
        Value::String(_) => 3,
        Value::Sequence(_) => 4,
        Value::Mapping(_) => 5,
        Value::Tagged(t) => type_rank(&t.value),
    }
}

fn is_null_or_missing(v: Option<&Value>) -> bool {
    matches!(v, None | Some(Value::Null))
}

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

    fn doc(pairs: Vec<(&str, Value)>) -> Mapping {
        let mut m = Mapping::new();
        for (k, v) in pairs {
            m.insert(Value::String(k.to_string()), v);
        }
        m
    }

    fn k(s: &str) -> Key {
        Key::name(s)
    }

    fn sort_with(field: &str, dir: SortDir) -> Sort {
        let path = if field.contains('.') {
            FieldPath::from_dotted(field)
        } else {
            FieldPath(vec![field.to_string()])
        };
        Sort { key: path, dir }
    }

    fn key_order(rows: &[(Key, Mapping)]) -> Vec<String> {
        rows.iter().map(|(k, _)| k.to_string()).collect()
    }

    #[test]
    fn ascending_orders_low_to_high() {
        let a = doc(vec![("priority", 1i64.into())]);
        let b = doc(vec![("priority", 5i64.into())]);
        let c = doc(vec![("priority", 3i64.into())]);
        let mut rows = vec![(k("a"), a), (k("b"), b), (k("c"), c)];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Asc));
        assert_eq!(key_order(&rows), vec!["a", "c", "b"]);
    }

    #[test]
    fn descending_orders_high_to_low() {
        let a = doc(vec![("priority", 1i64.into())]);
        let b = doc(vec![("priority", 5i64.into())]);
        let c = doc(vec![("priority", 3i64.into())]);
        let mut rows = vec![(k("a"), a), (k("b"), b), (k("c"), c)];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Desc));
        assert_eq!(key_order(&rows), vec!["b", "c", "a"]);
    }

    #[test]
    fn null_first_ascending() {
        let a = doc(vec![("priority", 1i64.into())]);
        let b = doc(vec![("priority", Value::Null)]);
        let c = Mapping::new();
        let mut rows = vec![(k("a"), a), (k("b"), b), (k("c"), c)];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Asc));
        let order = key_order(&rows);
        assert_eq!(order[2], "a");
        assert!(order[0..2].contains(&"b".to_string()));
        assert!(order[0..2].contains(&"c".to_string()));
    }

    #[test]
    fn null_last_descending() {
        let a = doc(vec![("priority", 1i64.into())]);
        let b = doc(vec![("priority", Value::Null)]);
        let c = Mapping::new();
        let mut rows = vec![(k("a"), a), (k("b"), b), (k("c"), c)];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Desc));
        let order = key_order(&rows);
        assert_eq!(order[0], "a");
        assert!(order[1..3].contains(&"b".to_string()));
        assert!(order[1..3].contains(&"c".to_string()));
    }

    #[test]
    fn mixed_type_field_is_total_order() {
        let a = doc(vec![("priority", 3i64.into())]);
        let b = doc(vec![("priority", "high".into())]);
        let c = doc(vec![("priority", 5i64.into())]);
        let d = doc(vec![("priority", "low".into())]);
        let e = doc(vec![("priority", 1i64.into())]);
        let mut rows = vec![
            (k("a"), a),
            (k("b"), b),
            (k("c"), c),
            (k("d"), d),
            (k("e"), e),
        ];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Asc));
        assert_eq!(key_order(&rows), vec!["e", "a", "c", "b", "d"]);
    }

    #[test]
    fn stable_sort_preserves_input_order_on_ties() {
        let a = doc(vec![("priority", 1i64.into())]);
        let b = doc(vec![("priority", 1i64.into())]);
        let c = doc(vec![("priority", 1i64.into())]);
        let mut rows = vec![(k("a"), a), (k("b"), b), (k("c"), c)];
        sort_in_place(&mut rows, &sort_with("priority", SortDir::Asc));
        assert_eq!(key_order(&rows), vec!["a", "b", "c"]);
    }
}