//! Aggregator state for running aggregates (sum, count).
//!
//! Shared between CLI engine and MCP server. Uses `HashMap<u64, f64>` for
//! grouped aggregation — group keys are hashed via FNV-1a to avoid per-record
//! String allocations (~5x less memory than `HashMap<String, f64>`).
//!
//! Reads raw numeric values directly from generators (no string parsing).

use std::collections::HashMap;
use std::fmt::Write;

use crate::tpl::column::{Column, ColumnGen};
use crate::tpl::AggrFunc;

struct AggrEntry {
    col_idx: usize,
    source_idx: usize,
    group_idx: Option<usize>,
    func: AggrFunc,
    global: f64,
    grouped: HashMap<u64, f64>,
}

pub struct AggrState {
    entries: Vec<AggrEntry>,
}

impl AggrState {
    pub fn new<S: AsRef<str>>(columns: &[Column], col_names: &[S]) -> Result<Self, String> {
        let mut entries = Vec::new();
        for (i, col) in columns.iter().enumerate() {
            if let ColumnGen::Aggr { func, source_col, group_by } = &col.gen {
                let source_idx =
                    col_names.iter().position(|n| n.as_ref() == source_col).ok_or_else(|| {
                        format!("aggregator source '{source_col}' not found in columns")
                    })?;
                let group_idx = group_by
                    .as_ref()
                    .map(|g| {
                        col_names
                            .iter()
                            .position(|n| n.as_ref() == g.as_str())
                            .ok_or_else(|| format!("aggregator group '{g}' not found in columns"))
                    })
                    .transpose()?;
                entries.push(AggrEntry {
                    col_idx: i,
                    source_idx,
                    group_idx,
                    func: *func,
                    global: 0.0,
                    grouped: HashMap::new(),
                });
            }
        }
        Ok(Self { entries })
    }

    /// Process all aggregators for the current record.
    /// `raw_values[i]` = Some(f64) for numeric fields, None for text fields.
    /// `values` = formatted string values (used only for group key hashing).
    pub fn update(
        &mut self,
        values: &mut [String],
        raw_values: &[Option<f64>],
    ) -> Result<(), String> {
        for entry in &mut self.entries {
            let src_raw = raw_values[entry.source_idx].ok_or_else(|| {
                format!("aggregator source column {} is not a numeric field", entry.source_idx)
            })?;
            let delta = match entry.func {
                AggrFunc::Sum => src_raw,
                AggrFunc::Count => 1.0,
            };
            let current = if let Some(gidx) = entry.group_idx {
                let key = seedfaker_core::hash_seed(&values[gidx]);
                let slot = entry.grouped.entry(key).or_insert(0.0);
                *slot += delta;
                *slot
            } else {
                entry.global += delta;
                entry.global
            };
            values[entry.col_idx].clear();
            match entry.func {
                AggrFunc::Count => {
                    let _ = write!(values[entry.col_idx], "{}", current as u64);
                }
                AggrFunc::Sum => {
                    let _ = write!(values[entry.col_idx], "{current:.2}");
                }
            }
        }
        Ok(())
    }
}
