//! Section aggregators for CalcPad sheets.
//!
//! Provides aggregation keywords (`sum`, `total`, `subtotal`, `average`/`avg`,
//! `min`, `max`, `count`) that operate over a section of lines, and
//! `grand total` which sums all subtotal results.
//!
//! A **section** is bounded by:
//! - Headings (lines starting with `#` followed by a space, e.g. `## Budget`)
//! - Other aggregator lines
//! - Start of document
//!
//! Only lines with numeric results are included in aggregation. Comments,
//! blank lines, and error lines are skipped.

use crate::span::Span;
use crate::types::{CalcResult, CalcValue};

/// The kind of aggregation to perform.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregatorKind {
    /// Sum of numeric values in the section. Also used for `total`.
    Sum,
    /// Distinct subtotal (tracked separately for grand total).
    Subtotal,
    /// Arithmetic mean of numeric values in the section.
    Average,
    /// Minimum numeric value in the section.
    Min,
    /// Maximum numeric value in the section.
    Max,
    /// Count of lines with numeric results in the section.
    Count,
    /// Sum of all subtotal values seen so far in the document.
    GrandTotal,
}

/// Check if a trimmed line is a heading (e.g., `## Monthly Costs`).
pub fn is_heading(line: &str) -> bool {
    let trimmed = line.trim();
    // Match lines starting with one or more '#' followed by a space
    let bytes = trimmed.as_bytes();
    if bytes.is_empty() || bytes[0] != b'#' {
        return false;
    }
    let mut i = 0;
    while i < bytes.len() && bytes[i] == b'#' {
        i += 1;
    }
    // Must have at least one # and be followed by a space (or be only #s)
    i > 0 && i <= 6 && (i >= bytes.len() || bytes[i] == b' ')
}

/// Detect if a trimmed line is a standalone aggregator keyword.
/// Returns the aggregator kind, or None if the line is not an aggregator.
pub fn detect_aggregator(line: &str) -> Option<AggregatorKind> {
    let trimmed = line.trim().to_lowercase();

    // Check for two-word "grand total" first
    if trimmed == "grand total" {
        return Some(AggregatorKind::GrandTotal);
    }

    match trimmed.as_str() {
        "sum" => Some(AggregatorKind::Sum),
        "total" => Some(AggregatorKind::Sum),
        "subtotal" => Some(AggregatorKind::Subtotal),
        "average" | "avg" => Some(AggregatorKind::Average),
        "min" => Some(AggregatorKind::Min),
        "max" => Some(AggregatorKind::Max),
        "count" => Some(AggregatorKind::Count),
        _ => None,
    }
}

/// Check if a line is a section boundary (heading or aggregator).
pub fn is_section_boundary(line: &str) -> bool {
    is_heading(line) || detect_aggregator(line).is_some()
}

/// Collect numeric values from the section above the given line index.
///
/// Walks backwards from the line before `line_index` (0-indexed) until hitting
/// a section boundary (heading, another aggregator, or start of document).
///
/// Only non-error results with extractable numeric values are included.
pub fn collect_section_values(
    results: &[CalcResult],
    sources: &[String],
    line_index: usize,
) -> Vec<f64> {
    let mut values = Vec::new();

    if line_index == 0 {
        return values;
    }

    for i in (0..line_index).rev() {
        let source = &sources[i];

        // Stop at headings
        if is_heading(source) {
            break;
        }

        // Stop at other aggregator lines
        if detect_aggregator(source).is_some() {
            break;
        }

        // Extract numeric value from result
        if let Some(val) = extract_numeric_value(&results[i]) {
            values.push(val);
        }
    }

    // Reverse to document order (we collected bottom-up)
    values.reverse();
    values
}

/// Extract a numeric value from a CalcResult, if it has one.
fn extract_numeric_value(result: &CalcResult) -> Option<f64> {
    match &result.value {
        CalcValue::Number { value } => Some(*value),
        CalcValue::UnitValue { value, .. } => Some(*value),
        CalcValue::CurrencyValue { amount, .. } => Some(*amount),
        _ => None,
    }
}

/// Compute an aggregation over the given values.
pub fn compute_aggregation(kind: AggregatorKind, values: &[f64], span: Span) -> CalcResult {
    match kind {
        AggregatorKind::GrandTotal => {
            // Grand total is handled separately (sums subtotals, not section values)
            let sum: f64 = values.iter().sum();
            CalcResult::number(sum, span)
        }
        _ => {
            if values.is_empty() {
                return CalcResult::number(0.0, span);
            }
            match kind {
                AggregatorKind::Sum | AggregatorKind::Subtotal => {
                    let sum: f64 = values.iter().sum();
                    CalcResult::number(sum, span)
                }
                AggregatorKind::Average => {
                    let sum: f64 = values.iter().sum();
                    CalcResult::number(sum / values.len() as f64, span)
                }
                AggregatorKind::Min => {
                    let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
                    CalcResult::number(min, span)
                }
                AggregatorKind::Max => {
                    let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
                    CalcResult::number(max, span)
                }
                AggregatorKind::Count => {
                    CalcResult::number(values.len() as f64, span)
                }
                AggregatorKind::GrandTotal => unreachable!(),
            }
        }
    }
}

/// Compute a grand total from a list of subtotal values.
pub fn compute_grand_total(subtotal_values: &[f64], span: Span) -> CalcResult {
    let sum: f64 = subtotal_values.iter().sum();
    CalcResult::number(sum, span)
}

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

    // --- is_heading ---

    #[test]
    fn test_heading_h1() {
        assert!(is_heading("# Title"));
    }

    #[test]
    fn test_heading_h2() {
        assert!(is_heading("## Subtitle"));
    }

    #[test]
    fn test_heading_h3_with_whitespace() {
        assert!(is_heading("  ### Indented Heading  "));
    }

    #[test]
    fn test_not_heading_hash_in_middle() {
        assert!(!is_heading("this is #not a heading"));
    }

    #[test]
    fn test_not_heading_hash_ref() {
        assert!(!is_heading("#1 * 2"));
    }

    #[test]
    fn test_not_heading_empty() {
        assert!(!is_heading(""));
    }

    // --- detect_aggregator ---

    #[test]
    fn test_detect_sum() {
        assert_eq!(detect_aggregator("sum"), Some(AggregatorKind::Sum));
    }

    #[test]
    fn test_detect_total() {
        assert_eq!(detect_aggregator("total"), Some(AggregatorKind::Sum));
    }

    #[test]
    fn test_detect_subtotal() {
        assert_eq!(detect_aggregator("subtotal"), Some(AggregatorKind::Subtotal));
    }

    #[test]
    fn test_detect_average() {
        assert_eq!(detect_aggregator("average"), Some(AggregatorKind::Average));
    }

    #[test]
    fn test_detect_avg() {
        assert_eq!(detect_aggregator("avg"), Some(AggregatorKind::Average));
    }

    #[test]
    fn test_detect_min() {
        assert_eq!(detect_aggregator("min"), Some(AggregatorKind::Min));
    }

    #[test]
    fn test_detect_max() {
        assert_eq!(detect_aggregator("max"), Some(AggregatorKind::Max));
    }

    #[test]
    fn test_detect_count() {
        assert_eq!(detect_aggregator("count"), Some(AggregatorKind::Count));
    }

    #[test]
    fn test_detect_grand_total() {
        assert_eq!(detect_aggregator("grand total"), Some(AggregatorKind::GrandTotal));
    }

    #[test]
    fn test_detect_case_insensitive() {
        assert_eq!(detect_aggregator("  SUM  "), Some(AggregatorKind::Sum));
        assert_eq!(detect_aggregator("  Grand Total  "), Some(AggregatorKind::GrandTotal));
    }

    #[test]
    fn test_detect_not_aggregator() {
        assert_eq!(detect_aggregator("sum + 5"), None);
        assert_eq!(detect_aggregator("total expense"), None);
        assert_eq!(detect_aggregator("x = 5"), None);
    }

    // --- collect_section_values ---

    #[test]
    fn test_collect_section_basic() {
        let results = vec![
            CalcResult::number(10.0, Span::new(0, 2)),
            CalcResult::number(20.0, Span::new(0, 2)),
            CalcResult::number(30.0, Span::new(0, 2)),
        ];
        let sources = vec!["10".to_string(), "20".to_string(), "30".to_string()];
        let values = collect_section_values(&results, &sources, 3);
        assert_eq!(values, vec![10.0, 20.0, 30.0]);
    }

    #[test]
    fn test_collect_section_stops_at_heading() {
        let results = vec![
            CalcResult::number(10.0, Span::new(0, 2)),
            CalcResult::error("heading", Span::new(0, 8)),
            CalcResult::number(20.0, Span::new(0, 2)),
            CalcResult::number(30.0, Span::new(0, 2)),
        ];
        let sources = vec![
            "10".to_string(),
            "## Section".to_string(),
            "20".to_string(),
            "30".to_string(),
        ];
        let values = collect_section_values(&results, &sources, 4);
        assert_eq!(values, vec![20.0, 30.0]);
    }

    #[test]
    fn test_collect_section_stops_at_aggregator() {
        let results = vec![
            CalcResult::number(10.0, Span::new(0, 2)),
            CalcResult::number(20.0, Span::new(0, 2)),
            CalcResult::number(30.0, Span::new(0, 3)), // sum result
            CalcResult::number(40.0, Span::new(0, 2)),
            CalcResult::number(50.0, Span::new(0, 2)),
        ];
        let sources = vec![
            "10".to_string(),
            "20".to_string(),
            "sum".to_string(),
            "40".to_string(),
            "50".to_string(),
        ];
        let values = collect_section_values(&results, &sources, 5);
        assert_eq!(values, vec![40.0, 50.0]);
    }

    #[test]
    fn test_collect_section_skips_errors() {
        let results = vec![
            CalcResult::number(10.0, Span::new(0, 2)),
            CalcResult::error("parse error", Span::new(0, 3)),
            CalcResult::number(30.0, Span::new(0, 2)),
        ];
        let sources = vec!["10".to_string(), "???".to_string(), "30".to_string()];
        let values = collect_section_values(&results, &sources, 3);
        assert_eq!(values, vec![10.0, 30.0]);
    }

    #[test]
    fn test_collect_section_empty() {
        let results = vec![
            CalcResult::error("heading", Span::new(0, 8)),
        ];
        let sources = vec!["## Section".to_string()];
        let values = collect_section_values(&results, &sources, 1);
        assert!(values.is_empty());
    }

    #[test]
    fn test_collect_at_start() {
        let values = collect_section_values(&[], &[], 0);
        assert!(values.is_empty());
    }

    // --- compute_aggregation ---

    #[test]
    fn test_sum_aggregation() {
        let values = vec![10.0, 20.0, 30.0, 40.0];
        let result = compute_aggregation(AggregatorKind::Sum, &values, Span::new(0, 3));
        assert_eq!(result.value, CalcValue::Number { value: 100.0 });
    }

    #[test]
    fn test_subtotal_aggregation() {
        let values = vec![10.0, 20.0, 30.0];
        let result = compute_aggregation(AggregatorKind::Subtotal, &values, Span::new(0, 8));
        assert_eq!(result.value, CalcValue::Number { value: 60.0 });
    }

    #[test]
    fn test_average_aggregation() {
        let values = vec![10.0, 20.0, 30.0];
        let result = compute_aggregation(AggregatorKind::Average, &values, Span::new(0, 7));
        assert_eq!(result.value, CalcValue::Number { value: 20.0 });
    }

    #[test]
    fn test_min_aggregation() {
        let values = vec![5.0, 12.0, 3.0, 8.0];
        let result = compute_aggregation(AggregatorKind::Min, &values, Span::new(0, 3));
        assert_eq!(result.value, CalcValue::Number { value: 3.0 });
    }

    #[test]
    fn test_max_aggregation() {
        let values = vec![5.0, 12.0, 3.0, 8.0];
        let result = compute_aggregation(AggregatorKind::Max, &values, Span::new(0, 3));
        assert_eq!(result.value, CalcValue::Number { value: 12.0 });
    }

    #[test]
    fn test_count_aggregation() {
        let values = vec![5.0, 12.0, 3.0, 8.0];
        let result = compute_aggregation(AggregatorKind::Count, &values, Span::new(0, 5));
        assert_eq!(result.value, CalcValue::Number { value: 4.0 });
    }

    #[test]
    fn test_empty_section_returns_zero() {
        let result = compute_aggregation(AggregatorKind::Sum, &[], Span::new(0, 3));
        assert_eq!(result.value, CalcValue::Number { value: 0.0 });

        let result = compute_aggregation(AggregatorKind::Average, &[], Span::new(0, 7));
        assert_eq!(result.value, CalcValue::Number { value: 0.0 });
    }

    // --- compute_grand_total ---

    #[test]
    fn test_grand_total_two_sections() {
        let subtotals = vec![300.0, 125.0];
        let result = compute_grand_total(&subtotals, Span::new(0, 11));
        assert_eq!(result.value, CalcValue::Number { value: 425.0 });
    }

    #[test]
    fn test_grand_total_empty() {
        let result = compute_grand_total(&[], Span::new(0, 11));
        assert_eq!(result.value, CalcValue::Number { value: 0.0 });
    }

    #[test]
    fn test_grand_total_includes_zero_subtotals() {
        let subtotals = vec![300.0, 0.0, 125.0];
        let result = compute_grand_total(&subtotals, Span::new(0, 11));
        assert_eq!(result.value, CalcValue::Number { value: 425.0 });
    }
}
