//! Columnar store reader: access column values by doc_id.
//!
//! See [[columnar-storage]] and [[feature-aggregations-v010#Step 1]].

use luci_core::FieldId;
use super::writer::{ColumnType, unpack_i64};

/// Per-column statistics for numeric columns (zonemaps).
#[derive(Clone, Debug)]
pub struct ColumnStats {
    pub null_count: u32,
    pub min: f64,
    pub max: f64,
}

/// Reads a single column's values from serialized bytes.
pub struct ColumnReader<'a> {
    data: &'a [u8],
    field_id: FieldId,
    col_type: ColumnType,
    doc_count: u32,
    null_bitset_start: usize,
    body_start: usize,
    /// For keyword columns: cached dictionary
    dict: Vec<&'a str>,
    /// For keyword columns: start of ordinal array
    ordinals_start: usize,
    /// For numeric columns: precomputed stats (zonemaps)
    stats: Option<ColumnStats>,
}

impl<'a> ColumnReader<'a> {
    pub fn open(data: &'a [u8]) -> Self {
        if data.len() < 7 {
            return Self::empty(data);
        }

        let field_id = FieldId::new(u16::from_le_bytes([data[0], data[1]]));
        let col_type_byte = data[2];
        let doc_count = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);

        let col_type = match col_type_byte {
            0 => ColumnType::Empty,
            1 => ColumnType::Keyword,
            2 => ColumnType::I64,
            3 => ColumnType::F64,
            4 => ColumnType::Bool,
            5 => ColumnType::ConstantI64,
            6 => ColumnType::ConstantF64,
            7 => ColumnType::BitpackedI64,
            _ => ColumnType::Empty,
        };

        if doc_count == 0 || col_type == ColumnType::Empty {
            return Self {
                data,
                field_id,
                col_type,
                doc_count: 0,
                null_bitset_start: 7,
                body_start: 7,
                dict: Vec::new(),
                ordinals_start: 0,
                stats: None,
            };
        }

        let null_bytes = (doc_count as usize + 7) / 8;
        let null_bitset_start = 7;
        let mut body_start = null_bitset_start + null_bytes;

        // Parse numeric stats if present
        let stats = if col_type.is_numeric() && body_start + 20 <= data.len() {
            let null_count = u32::from_le_bytes(
                data[body_start..body_start + 4].try_into().unwrap(),
            );
            let min = f64::from_le_bytes(
                data[body_start + 4..body_start + 12].try_into().unwrap(),
            );
            let max = f64::from_le_bytes(
                data[body_start + 12..body_start + 20].try_into().unwrap(),
            );
            body_start += 20; // skip past stats
            Some(ColumnStats { null_count, min, max })
        } else {
            None
        };

        let mut reader = Self {
            data,
            field_id,
            col_type,
            doc_count,
            null_bitset_start,
            body_start,
            dict: Vec::new(),
            ordinals_start: 0,
            stats,
        };

        if col_type == ColumnType::Keyword {
            reader.parse_keyword_dict();
        }

        reader
    }

    fn empty(data: &'a [u8]) -> Self {
        Self {
            data,
            field_id: FieldId::new(0),
            col_type: ColumnType::Empty,
            doc_count: 0,
            null_bitset_start: 0,
            body_start: 0,
            dict: Vec::new(),
            ordinals_start: 0,
            stats: None,
        }
    }

    fn parse_keyword_dict(&mut self) {
        let mut pos = self.body_start;
        let num_entries = u32::from_le_bytes(
            self.data[pos..pos + 4].try_into().unwrap(),
        ) as usize;
        pos += 4;

        self.dict = Vec::with_capacity(num_entries);
        for _ in 0..num_entries {
            let len = u16::from_le_bytes(
                self.data[pos..pos + 2].try_into().unwrap(),
            ) as usize;
            pos += 2;
            let s = std::str::from_utf8(&self.data[pos..pos + len]).unwrap();
            pos += len;
            self.dict.push(s);
        }
        self.ordinals_start = pos;
    }

    pub fn field_id(&self) -> FieldId {
        self.field_id
    }

    pub fn col_type(&self) -> ColumnType {
        self.col_type
    }

    pub fn doc_count(&self) -> u32 {
        self.doc_count
    }

    pub fn is_null(&self, doc_id: u32) -> bool {
        if doc_id >= self.doc_count {
            return true;
        }
        let byte_idx = self.null_bitset_start + (doc_id as usize / 8);
        let bit_idx = doc_id as usize % 8;
        (self.data[byte_idx] >> bit_idx) & 1 == 1
    }

    /// Get the raw dictionary ordinal for a keyword field (no string decode).
    pub fn keyword_ordinal(&self, doc_id: u32) -> Option<u32> {
        if doc_id >= self.doc_count || self.is_null(doc_id) {
            return None;
        }
        let pos = self.ordinals_start + doc_id as usize * 4;
        let ordinal = u32::from_le_bytes(
            self.data[pos..pos + 4].try_into().unwrap(),
        );
        if ordinal == u32::MAX { None } else { Some(ordinal) }
    }

    /// Number of unique values in the keyword dictionary.
    pub fn dict_size(&self) -> usize {
        self.dict.len()
    }

    /// Resolve an ordinal back to its string value.
    pub fn ordinal_to_string(&self, ordinal: u32) -> Option<&'a str> {
        self.dict.get(ordinal as usize).copied()
    }

    pub fn keyword_value(&self, doc_id: u32) -> Option<&'a str> {
        if doc_id >= self.doc_count || self.is_null(doc_id) {
            return None;
        }
        let pos = self.ordinals_start + doc_id as usize * 4;
        let ordinal = u32::from_le_bytes(
            self.data[pos..pos + 4].try_into().unwrap(),
        );
        if ordinal == u32::MAX {
            return None;
        }
        Some(self.dict[ordinal as usize])
    }

    pub fn i64_value(&self, doc_id: u32) -> Option<i64> {
        if doc_id >= self.doc_count || self.is_null(doc_id) {
            return None;
        }
        if self.col_type == ColumnType::BitpackedI64 {
            let min_val = i64::from_le_bytes(
                self.data[self.body_start..self.body_start + 8].try_into().unwrap(),
            );
            let bit_width = self.data[self.body_start + 8];
            let packed_start = self.body_start + 9;
            return Some(unpack_i64(&self.data[packed_start..], doc_id as usize, min_val, bit_width));
        }
        let pos = self.body_start + doc_id as usize * 8;
        Some(i64::from_le_bytes(
            self.data[pos..pos + 8].try_into().unwrap(),
        ))
    }

    pub fn f64_value(&self, doc_id: u32) -> Option<f64> {
        if doc_id >= self.doc_count || self.is_null(doc_id) {
            return None;
        }
        let pos = self.body_start + doc_id as usize * 8;
        Some(f64::from_le_bytes(
            self.data[pos..pos + 8].try_into().unwrap(),
        ))
    }

    pub fn bool_value(&self, doc_id: u32) -> Option<bool> {
        if doc_id >= self.doc_count || self.is_null(doc_id) {
            return None;
        }
        let byte_idx = self.body_start + (doc_id as usize / 8);
        let bit_idx = doc_id as usize % 8;
        Some((self.data[byte_idx] >> bit_idx) & 1 == 1)
    }

    /// Get the numeric value as f64, regardless of underlying type.
    /// Converts i64 → f64. Returns None for non-numeric, null, or constant
    /// columns. For constant columns, use `is_constant()` + `constant_value()`.
    pub fn numeric_value(&self, doc_id: u32) -> Option<f64> {
        match self.col_type {
            ColumnType::I64 | ColumnType::BitpackedI64 => self.i64_value(doc_id).map(|v| v as f64),
            ColumnType::F64 => self.f64_value(doc_id),
            _ => None,
        }
    }

    /// Returns true if this column uses constant encoding (all non-null
    /// values are identical). Enables O(1) aggregation fast-paths.
    pub fn is_constant(&self) -> bool {
        matches!(self.col_type, ColumnType::ConstantI64 | ColumnType::ConstantF64)
    }

    /// Get the constant value if this is a constant-encoded column.
    pub fn constant_value(&self) -> Option<f64> {
        match self.col_type {
            ColumnType::ConstantI64 => {
                Some(i64::from_le_bytes(
                    self.data[self.body_start..self.body_start + 8].try_into().unwrap(),
                ) as f64)
            }
            ColumnType::ConstantF64 => {
                Some(f64::from_le_bytes(
                    self.data[self.body_start..self.body_start + 8].try_into().unwrap(),
                ))
            }
            _ => None,
        }
    }

    /// Get precomputed column statistics (min, max, null_count) for numeric
    /// columns. Returns None for non-numeric types. Enables O(1) aggregation
    /// pushdown for min/max/count on unfiltered queries.
    pub fn stats(&self) -> Option<&ColumnStats> {
        self.stats.as_ref()
    }
}

/// Reads the columnar component of a segment — multiple columns.
pub struct ColumnarReader<'a> {
    data: &'a [u8],
    columns: Vec<(FieldId, usize, usize)>, // (field_id, start_offset, end_offset)
}

impl<'a> ColumnarReader<'a> {
    pub fn open(data: &'a [u8]) -> Self {
        if data.len() < 2 {
            return Self {
                data,
                columns: Vec::new(),
            };
        }

        let num_columns = u16::from_le_bytes([data[0], data[1]]) as usize;
        let mut pos = 2usize;
        let mut columns = Vec::with_capacity(num_columns);

        for _ in 0..num_columns {
            let start = pos;
            // Parse enough to find the end of this column
            if pos + 7 > data.len() {
                break;
            }
            let field_id = FieldId::new(u16::from_le_bytes([data[pos], data[pos + 1]]));
            let col_type = data[pos + 2];
            let doc_count = u32::from_le_bytes(
                data[pos + 3..pos + 7].try_into().unwrap(),
            ) as usize;
            pos += 7;

            if doc_count == 0 || col_type == 0 {
                columns.push((field_id, start, pos));
                continue;
            }

            // Skip null bitset
            let null_bytes = (doc_count + 7) / 8;
            pos += null_bytes;

            // Skip numeric stats (null_count + min + max = 20 bytes)
            let is_numeric = matches!(col_type, 2 | 3 | 5 | 6 | 7);
            if is_numeric {
                pos += 20;
            }

            // Skip body based on type
            match col_type {
                1 => {
                    // Keyword: dictionary + ordinals
                    let num_entries = u32::from_le_bytes(
                        data[pos..pos + 4].try_into().unwrap(),
                    ) as usize;
                    pos += 4;
                    for _ in 0..num_entries {
                        let len = u16::from_le_bytes(
                            data[pos..pos + 2].try_into().unwrap(),
                        ) as usize;
                        pos += 2 + len;
                    }
                    pos += doc_count * 4; // ordinals
                }
                2 | 3 => {
                    // i64 or f64: 8 bytes per doc
                    pos += doc_count * 8;
                }
                4 => {
                    // Bool: bitset
                    pos += (doc_count + 7) / 8;
                }
                5 | 6 => {
                    // ConstantI64 or ConstantF64: single 8-byte value
                    pos += 8;
                }
                7 => {
                    // BitpackedI64: min(8) + bit_width(1) + packed data
                    let bit_width = data[pos + 8] as usize;
                    pos += 9; // min + bit_width
                    pos += (doc_count * bit_width + 7) / 8; // packed residuals
                }
                _ => {}
            }

            columns.push((field_id, start, pos));
        }

        Self { data, columns }
    }

    /// Get a column reader for a specific field.
    pub fn column(&self, field_id: FieldId) -> Option<ColumnReader<'a>> {
        for &(fid, start, end) in &self.columns {
            if fid == field_id {
                return Some(ColumnReader::open(&self.data[start..end]));
            }
        }
        None
    }
}
