//! User-state filter extraction from filter expression trees.
//!
//! Walks a `FilterExpr` AST and collects references to user-state filter
//! variants (`Saved`, `Liked`, `InProgress`). These variants pass through
//! the `FilterEvaluator` as "return full universe" and must be applied as
//! inclusion filters in Stage 2.5 of the executor pipeline, where
//! `UserStateIndex` is available.

use crate::storage::indexes::filter::FilterExpr;

/// Extract user-state filter variants from a filter expression tree.
///
/// AND/OR/NOT nodes are recursed into; leaf nodes that are not user-state
/// filters are ignored (they are already handled by the `FilterEvaluator`).
pub(crate) fn extract_user_state_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    let mut result = Vec::new();
    collect_user_state_filters(expr, &mut result);
    result
}

fn collect_user_state_filters<'a>(expr: &'a FilterExpr, out: &mut Vec<&'a FilterExpr>) {
    match expr {
        FilterExpr::Saved(_)
        | FilterExpr::Liked(_)
        | FilterExpr::InProgress { .. }
        | FilterExpr::SocialGraph { .. } => {
            out.push(expr);
        }
        FilterExpr::And(children) | FilterExpr::Or(children) => {
            for child in children {
                collect_user_state_filters(child, out);
            }
        }
        FilterExpr::Not(inner) => {
            collect_user_state_filters(inner, out);
        }
        // All other variants (CategoryEq, FormatEq, etc.) are handled by
        // FilterEvaluator and do not need extraction here.
        _ => {}
    }
}

// ── M6p3 Signal Threshold + Geo Filter Extraction ────────────────────────────

/// Extract `MinSignal` and `MaxSignal` filter variants from a filter expression tree.
///
/// Recursively walks AND/OR/NOT nodes. These variants pass through the
/// `FilterEvaluator` as "return full universe" and are applied as post-filters
/// in Stage 2.3 of the executor pipeline, where the signal ledger is available.
pub(crate) fn extract_signal_threshold_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    let mut result = Vec::new();
    collect_signal_threshold_filters(expr, &mut result);
    result
}

fn collect_signal_threshold_filters<'a>(expr: &'a FilterExpr, out: &mut Vec<&'a FilterExpr>) {
    match expr {
        FilterExpr::MinSignal { .. } | FilterExpr::MaxSignal { .. } => {
            out.push(expr);
        }
        FilterExpr::And(children) | FilterExpr::Or(children) => {
            // NOTE: Both AND and OR branches are walked the same way — all
            // MinSignal/MaxSignal variants are extracted and then applied
            // conjunctively in Stage 2.3. This means `OR(MinSignal(A,10),
            // MinSignal(B,5))` is evaluated as AND (both must pass), which is
            // semantically incorrect for OR expressions. In practice, engagement
            // threshold filters are always ANDed, so this limitation is benign.
            // Fix if composable OR-over-signals becomes a use case.
            for child in children {
                collect_signal_threshold_filters(child, out);
            }
        }
        FilterExpr::Not(inner) => {
            collect_signal_threshold_filters(inner, out);
        }
        _ => {}
    }
}

/// Extract `NearLocation` filter variants from a filter expression tree.
///
/// Same recursive walk as `extract_signal_threshold_filters`. Applied as
/// post-filters in Stage 2.4 of the executor pipeline, where item metadata
/// (latitude/longitude) is read from storage.
pub(crate) fn extract_geo_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    let mut result = Vec::new();
    collect_geo_filters(expr, &mut result);
    result
}

fn collect_geo_filters<'a>(expr: &'a FilterExpr, out: &mut Vec<&'a FilterExpr>) {
    match expr {
        FilterExpr::NearLocation { .. } => {
            out.push(expr);
        }
        FilterExpr::And(children) | FilterExpr::Or(children) => {
            for child in children {
                collect_geo_filters(child, out);
            }
        }
        FilterExpr::Not(inner) => {
            collect_geo_filters(inner, out);
        }
        _ => {}
    }
}

// ── M6p4 Collection Filter Extraction ─────────────────────────────────────────

/// Extract `InCollection` filter variants from a filter expression tree.
///
/// Recursively walks AND/OR/NOT nodes. These variants pass through the
/// `FilterEvaluator` as "return full universe" and are applied as post-filters
/// in Stage 2.2 of the executor pipeline, where the `CollectionIndex` is available.
pub(crate) fn extract_collection_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    let mut result = Vec::new();
    collect_collection_filters(expr, &mut result);
    result
}

fn collect_collection_filters<'a>(expr: &'a FilterExpr, out: &mut Vec<&'a FilterExpr>) {
    match expr {
        FilterExpr::InCollection(_) => {
            out.push(expr);
        }
        FilterExpr::And(children) | FilterExpr::Or(children) => {
            for child in children {
                collect_collection_filters(child, out);
            }
        }
        FilterExpr::Not(inner) => {
            collect_collection_filters(inner, out);
        }
        _ => {}
    }
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::storage::indexes::filter::FilterExpr;

    #[test]
    fn extract_user_state_filters_finds_saved_liked_in_progress() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::Saved(42),
            FilterExpr::Liked(42),
            FilterExpr::InProgress {
                user_id: 42,
                threshold: 0.8,
            },
        ]);
        let found = extract_user_state_filters(&expr);
        assert_eq!(found.len(), 3);
        assert!(matches!(found[0], FilterExpr::Saved(42)));
        assert!(matches!(found[1], FilterExpr::Liked(42)));
        assert!(matches!(
            found[2],
            FilterExpr::InProgress {
                user_id: 42,
                threshold,
            } if (*threshold - 0.8).abs() < f64::EPSILON
        ));
    }

    #[test]
    fn extract_user_state_filters_empty_for_plain_filters() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::FormatEq("video".into()),
        ]);
        let found = extract_user_state_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_user_state_filters_nested_in_or() {
        let expr = FilterExpr::Or(vec![
            FilterExpr::Saved(1),
            FilterExpr::Not(Box::new(FilterExpr::Liked(2))),
        ]);
        let found = extract_user_state_filters(&expr);
        assert_eq!(found.len(), 2);
    }

    // ── Signal threshold extraction ─────────────────────────────────────────

    #[test]
    fn extract_signal_threshold_finds_min_and_max() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::MinSignal {
                signal: "view".into(),
                threshold: 100.0,
            },
            FilterExpr::MaxSignal {
                signal: "hide".into(),
                threshold: 5.0,
            },
        ]);
        let found = extract_signal_threshold_filters(&expr);
        assert_eq!(found.len(), 2);
        assert!(matches!(found[0], FilterExpr::MinSignal { .. }));
        assert!(matches!(found[1], FilterExpr::MaxSignal { .. }));
    }

    #[test]
    fn extract_signal_threshold_empty_for_plain_filters() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("rock".into()),
            FilterExpr::FormatEq("audio".into()),
        ]);
        let found = extract_signal_threshold_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_signal_threshold_nested_in_not() {
        let expr = FilterExpr::Not(Box::new(FilterExpr::MinSignal {
            signal: "like".into(),
            threshold: 10.0,
        }));
        let found = extract_signal_threshold_filters(&expr);
        assert_eq!(found.len(), 1);
    }

    // ── Geo filter extraction ───────────────────────────────────────────────

    #[test]
    fn extract_geo_finds_near_location() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("food".into()),
            FilterExpr::NearLocation {
                lat: 40.7128,
                lng: -74.0060,
                radius_km: 10.0,
            },
        ]);
        let found = extract_geo_filters(&expr);
        assert_eq!(found.len(), 1);
        assert!(matches!(found[0], FilterExpr::NearLocation { .. }));
    }

    #[test]
    fn extract_geo_empty_for_plain_filters() {
        let expr = FilterExpr::Tag("live".into());
        let found = extract_geo_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_geo_nested_in_or() {
        let expr = FilterExpr::Or(vec![
            FilterExpr::NearLocation {
                lat: 34.0522,
                lng: -118.2437,
                radius_km: 5.0,
            },
            FilterExpr::NearLocation {
                lat: 40.7128,
                lng: -74.0060,
                radius_km: 15.0,
            },
        ]);
        let found = extract_geo_filters(&expr);
        assert_eq!(found.len(), 2);
    }

    // ── Collection filter extraction ─────────────────────────────────────────

    #[test]
    fn extract_collection_finds_in_collection() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::InCollection(crate::entities::CollectionId::new(42)),
        ]);
        let found = extract_collection_filters(&expr);
        assert_eq!(found.len(), 1);
        assert!(matches!(found[0], FilterExpr::InCollection(_)));
    }

    #[test]
    fn extract_collection_empty_for_plain_filters() {
        let expr = FilterExpr::Tag("live".into());
        let found = extract_collection_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_collection_nested_in_not() {
        let expr = FilterExpr::Not(Box::new(FilterExpr::InCollection(
            crate::entities::CollectionId::new(1),
        )));
        let found = extract_collection_filters(&expr);
        assert_eq!(found.len(), 1);
    }
}
