//! Python bindings for mf4-rs using PyO3
//!
//! This module provides Python bindings for the main functionality of mf4-rs:
//! - Parsing MDF files
//! - Writing MDF files
//! - Creating and using indexes

use pyo3::prelude::*;
use pyo3::types::IntoPyDict;
use pyo3::{create_exception, wrap_pyfunction};
use numpy::{PyArray1, PyReadonlyArray1};
use std::collections::HashMap;

use crate::api::mdf::MDF;
use crate::writer::{MdfWriter, ColumnData};
use crate::index::{MdfIndex, FileRangeReader, IndexedChannel};
use crate::blocks::common::DataType;
use crate::parsing::decoder::DecodedValue;
use crate::error::MdfError;
use crate::block_layout::{BlockInfo, FileLayout, GapInfo, LinkInfo};

// Custom exception for MDF errors
create_exception!(mf4_rs, MdfException, pyo3::exceptions::PyException);

// Convert Rust MdfError to Python exception
impl From<MdfError> for PyErr {
    fn from(err: MdfError) -> PyErr {
        MdfException::new_err(format!("{:?}", err))
    }
}

// Python-friendly data type enum
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyDataType {
    #[pyo3(get)]
    pub name: String,
    #[pyo3(get)]
    pub value: u8,
}

#[pymethods]
impl PyDataType {
    fn __str__(&self) -> String {
        self.name.clone()
    }
    
    fn __repr__(&self) -> String {
        format!("DataType({})", self.name)
    }
}

impl From<&DataType> for PyDataType {
    fn from(dt: &DataType) -> Self {
        let (name, value) = match dt {
            DataType::UnsignedIntegerLE => ("UnsignedIntegerLE", 0),
            DataType::UnsignedIntegerBE => ("UnsignedIntegerBE", 1),
            DataType::SignedIntegerLE => ("SignedIntegerLE", 2),
            DataType::SignedIntegerBE => ("SignedIntegerBE", 3),
            DataType::FloatLE => ("FloatLE", 4),
            DataType::FloatBE => ("FloatBE", 5),
            DataType::StringLatin1 => ("StringLatin1", 6),
            DataType::StringUtf8 => ("StringUtf8", 7),
            DataType::StringUtf16LE => ("StringUtf16LE", 8),
            DataType::StringUtf16BE => ("StringUtf16BE", 9),
            DataType::ByteArray => ("ByteArray", 10),
            DataType::MimeSample => ("MimeSample", 11),
            DataType::MimeStream => ("MimeStream", 12),
            DataType::CanOpenDate => ("CanOpenDate", 13),
            DataType::CanOpenTime => ("CanOpenTime", 14),
            DataType::ComplexLE => ("ComplexLE", 15),
            DataType::ComplexBE => ("ComplexBE", 16),
            DataType::Unknown(_) => ("Unknown", 255),
        };
        PyDataType {
            name: name.to_string(),
            value,
        }
    }
}

impl From<PyDataType> for DataType {
    fn from(py_dt: PyDataType) -> Self {
        match py_dt.value {
            0 => DataType::UnsignedIntegerLE,
            1 => DataType::UnsignedIntegerBE,
            2 => DataType::SignedIntegerLE,
            3 => DataType::SignedIntegerBE,
            4 => DataType::FloatLE,
            5 => DataType::FloatBE,
            6 => DataType::StringLatin1,
            7 => DataType::StringUtf8,
            8 => DataType::StringUtf16LE,
            9 => DataType::StringUtf16BE,
            10 => DataType::ByteArray,
            11 => DataType::MimeSample,
            12 => DataType::MimeStream,
            13 => DataType::CanOpenDate,
            14 => DataType::CanOpenTime,
            15 => DataType::ComplexLE,
            16 => DataType::ComplexBE,
            _ => DataType::Unknown(()),
        }
    }
}

// Python-friendly decoded value
#[pyclass]
#[derive(Debug, Clone)]
pub enum PyDecodedValue {
    Float { value: f64 },
    UnsignedInteger { value: u64 },
    SignedInteger { value: i64 },
    String { value: String },
    ByteArray { value: Vec<u8> },
    Unknown { },
}

#[pymethods]
impl PyDecodedValue {
    fn __str__(&self) -> String {
        match self {
            PyDecodedValue::Float { value } => format!("{}", value),
            PyDecodedValue::UnsignedInteger { value } => format!("{}", value),
            PyDecodedValue::SignedInteger { value } => format!("{}", value),
            PyDecodedValue::String { value } => value.clone(),
            PyDecodedValue::ByteArray { value } => format!("bytes[{}]", value.len()),
            PyDecodedValue::Unknown { } => "Unknown".to_string(),
        }
    }
    
    fn __repr__(&self) -> String {
        match self {
            PyDecodedValue::Float { value } => format!("Float({})", value),
            PyDecodedValue::UnsignedInteger { value } => format!("UnsignedInteger({})", value),
            PyDecodedValue::SignedInteger { value } => format!("SignedInteger({})", value),
            PyDecodedValue::String { value } => format!("String('{}')", value),
            PyDecodedValue::ByteArray { value } => format!("ByteArray({})", value.len()),
            PyDecodedValue::Unknown { } => "Unknown".to_string(),
        }
    }
    
    #[getter]
    fn value(&self, py: Python) -> PyObject {
        match self {
            PyDecodedValue::Float { value } => value.to_object(py),
            PyDecodedValue::UnsignedInteger { value } => value.to_object(py),
            PyDecodedValue::SignedInteger { value } => value.to_object(py),
            PyDecodedValue::String { value } => value.to_object(py),
            PyDecodedValue::ByteArray { value } => value.to_object(py),
            PyDecodedValue::Unknown { } => py.None(),
        }
    }
}

impl From<DecodedValue> for PyDecodedValue {
    fn from(dv: DecodedValue) -> Self {
        match dv {
            DecodedValue::Float(v) => PyDecodedValue::Float { value: v },
            DecodedValue::UnsignedInteger(v) => PyDecodedValue::UnsignedInteger { value: v },
            DecodedValue::SignedInteger(v) => PyDecodedValue::SignedInteger { value: v },
            DecodedValue::String(v) => PyDecodedValue::String { value: v },
            DecodedValue::ByteArray(v) => PyDecodedValue::ByteArray { value: v },
            DecodedValue::MimeSample(v) => PyDecodedValue::ByteArray { value: v },
            DecodedValue::MimeStream(v) => PyDecodedValue::ByteArray { value: v },
            DecodedValue::Unknown => PyDecodedValue::Unknown { },
        }
    }
}

impl From<PyDecodedValue> for DecodedValue {
    fn from(py_dv: PyDecodedValue) -> Self {
        match py_dv {
            PyDecodedValue::Float { value } => DecodedValue::Float(value),
            PyDecodedValue::UnsignedInteger { value } => DecodedValue::UnsignedInteger(value),
            PyDecodedValue::SignedInteger { value } => DecodedValue::SignedInteger(value),
            PyDecodedValue::String { value } => DecodedValue::String(value),
            PyDecodedValue::ByteArray { value } => DecodedValue::ByteArray(value),
            PyDecodedValue::Unknown { } => DecodedValue::Unknown,
        }
    }
}

/// Direct conversion from DecodedValue to PyObject without intermediate allocation.
/// This is more efficient than creating a PyDecodedValue and immediately extracting its value.
fn decoded_value_to_pyobject(dv: DecodedValue, py: Python) -> PyObject {
    match dv {
        DecodedValue::Float(v) => v.to_object(py),
        DecodedValue::UnsignedInteger(v) => v.to_object(py),
        DecodedValue::SignedInteger(v) => v.to_object(py),
        DecodedValue::String(v) => v.to_object(py),
        DecodedValue::ByteArray(v) | DecodedValue::MimeSample(v) | DecodedValue::MimeStream(v) => {
            v.to_object(py)
        }
        DecodedValue::Unknown => py.None(),
    }
}

// Simplified Channel info structure to avoid lifetime issues
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyChannelInfo {
    #[pyo3(get)]
    pub name: Option<String>,
    #[pyo3(get)] 
    pub unit: Option<String>,
    #[pyo3(get)]
    pub comment: Option<String>,
    #[pyo3(get)]
    pub data_type: PyDataType,
    #[pyo3(get)]
    pub bit_count: u32,
}

#[pymethods]
impl PyChannelInfo {
    fn __str__(&self) -> String {
        format!("Channel(name={:?}, data_type={}, bit_count={})", 
                self.name, self.data_type.name, self.bit_count)
    }
    
    fn __repr__(&self) -> String {
        self.__str__()
    }
}

// Simplified ChannelGroup info structure
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyChannelGroupInfo {
    #[pyo3(get)]
    pub name: Option<String>,
    #[pyo3(get)]
    pub comment: Option<String>,
    #[pyo3(get)]
    pub channel_count: usize,
    #[pyo3(get)]
    pub record_count: u64,
}

#[pymethods]
impl PyChannelGroupInfo {
    fn __str__(&self) -> String {
        format!("ChannelGroup(name={:?}, channels={}, records={})", 
                self.name, self.channel_count, self.record_count)
    }
    
    fn __repr__(&self) -> String {
        self.__str__()
    }
}

/// Helper function to check if pandas is available and import it
fn check_pandas_available(py: Python) -> PyResult<PyObject> {
    py.import_bound("pandas")
        .map(|m| m.into())
        .map_err(|_| MdfException::new_err(
            "Pandas is not installed. Please install pandas to use Series-based methods."
        ))
}

/// Helper function to find the master/time channel in a channel group
/// Returns None if no master channel is found
fn find_master_channel<'a>(channels: &Vec<crate::api::channel::Channel<'a>>) -> Option<usize> {
    for (idx, channel) in channels.iter().enumerate() {
        let block = channel.block();
        // Master channels have channel_type == 2
        if block.channel_type == 2 {
            return Some(idx);
        }
    }
    None
}

/// Helper function to find the master/time channel in indexed channels
/// Returns None if no master channel is found
fn find_master_channel_indexed(channels: &Vec<IndexedChannel>) -> Option<usize> {
    for (idx, channel) in channels.iter().enumerate() {
        // Master channels have channel_type == 2
        if channel.channel_type == 2 {
            return Some(idx);
        }
    }
    None
}

/// Helper function to create a pandas DatetimeIndex from relative time values
///
/// # Arguments
/// * `py` - Python GIL token
/// * `pd` - pandas module PyObject
/// * `relative_times` - Vector of relative time values (in seconds) as PyObjects
/// * `start_ns` - Start time in nanoseconds since epoch
///
/// # Returns
/// PyObject representing a pandas DatetimeIndex, or an error if conversion fails
///
/// # Error Handling
/// This function handles multiple edge cases:
/// - None values in master channel (converted to NaT - Not a Time)
/// - Integer time values (i64, u64) in addition to float values
/// - Negative time values that would create timestamps before epoch
/// - Overflow/underflow when adding large time deltas
fn create_datetime_index(
    py: Python,
    pd: &PyObject,
    relative_times: &[PyObject],
    start_ns: u64,
) -> PyResult<PyObject> {
    // Convert start time from nanoseconds to a pandas Timestamp
    let to_datetime = pd.getattr(py, "to_datetime")?;
    let start_timestamp = to_datetime.call(
        py,
        (start_ns,),
        Some([("unit", "ns")].into_py_dict(py))
    )?;

    // Create a Timedelta for each relative time and add to start time
    let timedelta_class = pd.getattr(py, "Timedelta")?;
    let nat = pd.getattr(py, "NaT")?;  // Not-a-Time for None values
    let mut absolute_times = Vec::with_capacity(relative_times.len());

    for rel_time in relative_times {
        // Handle None values (invalid samples) - convert to NaT
        if rel_time.is_none(py) {
            absolute_times.push(nat.clone());
            continue;
        }

        // Try to extract the time value as f64, then i64, then u64
        // Master channels can be float or integer type
        let seconds: f64 = if let Ok(f) = rel_time.extract::<f64>(py) {
            f
        } else if let Ok(i) = rel_time.extract::<i64>(py) {
            i as f64
        } else if let Ok(u) = rel_time.extract::<u64>(py) {
            u as f64
        } else {
            // If we can't extract as any numeric type, use NaT
            absolute_times.push(nat.clone());
            continue;
        };

        // Check for edge cases that might cause overflow or invalid timestamps
        // pandas datetime64[ns] has range: 1678-2262 (approximately)
        // If start_ns + seconds would overflow, pandas will raise an OutOfBoundsDatetime error
        // We'll let pandas handle this and propagate the error up

        // Create a Timedelta in seconds and add to start time
        // This can fail if the resulting timestamp is out of pandas' valid range
        let delta = timedelta_class.call(
            py,
            (seconds,),
            Some([("unit", "s")].into_py_dict(py))
        )?;

        let absolute_time = start_timestamp.call_method1(py, "__add__", (delta,))?;
        absolute_times.push(absolute_time);
    }

    // Create DatetimeIndex from the list of timestamps
    let datetime_index_class = pd.getattr(py, "DatetimeIndex")?;
    let datetime_index = datetime_index_class.call1(py, (absolute_times,))?;

    Ok(datetime_index)
}

// Python wrapper for MDF
#[pyclass]
pub struct PyMDF {
    mdf: Box<MDF>,
}

#[pymethods]
impl PyMDF {
    /// Create a new PyMDF from a file path
    #[new]
    fn new(path: &str) -> PyResult<Self> {
        let mdf = Box::new(MDF::from_file(path)?);
        Ok(PyMDF { mdf })
    }
    
    /// Get all channel groups info
    fn channel_groups(&self) -> PyResult<Vec<PyChannelGroupInfo>> {
        let mut py_groups = Vec::new();
        
        for group in self.mdf.channel_groups() {
            let name = group.name()?;
            let comment = group.comment()?;
            let channel_count = group.channels().len();
            let record_count = group.raw_channel_group().block.cycles_nr;
            
            py_groups.push(PyChannelGroupInfo {
                name,
                comment,
                channel_count,
                record_count,
            });
        }
        
        Ok(py_groups)
    }
    
    /// Get channel info for all channels in all groups
    fn get_all_channels(&self) -> PyResult<Vec<PyChannelInfo>> {
        let mut channels = Vec::new();
        
        for group in self.mdf.channel_groups() {
            for channel in group.channels() {
                let block = channel.block();
                let info = PyChannelInfo {
                    name: channel.name()?,
                    unit: channel.unit()?,
                    comment: channel.comment()?,
                    data_type: PyDataType::from(&block.data_type),
                    bit_count: block.bit_count,
                };
                channels.push(info);
            }
        }
        
        Ok(channels)
    }
    
    /// Get channels for a specific group by index
    fn get_channels_for_group(&self, group_index: usize) -> PyResult<Vec<PyChannelInfo>> {
        let groups: Vec<_> = self.mdf.channel_groups().into_iter().collect();
        if let Some(group) = groups.get(group_index) {
            let mut channels = Vec::new();
            
            for channel in group.channels() {
                let block = channel.block();
                let info = PyChannelInfo {
                    name: channel.name()?,
                    unit: channel.unit()?,
                    comment: channel.comment()?,
                    data_type: PyDataType::from(&block.data_type),
                    bit_count: block.bit_count,
                };
                channels.push(info);
            }
            
            Ok(channels)
        } else {
            Err(MdfException::new_err("Group index out of bounds"))
        }
    }
    
    /// Get channel names from all groups
    fn get_all_channel_names(&self) -> PyResult<Vec<String>> {
        let mut names = Vec::new();
        for group in self.mdf.channel_groups() {
            for channel in group.channels() {
                if let Some(name) = channel.name()? {
                    names.push(name);
                }
            }
        }
        Ok(names)
    }
    
    /// Get channel values by name (first match) as a numpy float64 array.
    ///
    /// Returns channel data directly as a numpy ndarray. This is the fastest
    /// way to read numeric channels. Non-decodable values are returned as NaN.
    ///
    /// Returns None if the channel is not found.
    fn get_channel_values<'py>(&self, py: Python<'py>, channel_name: &str) -> PyResult<Option<PyObject>> {
        for group in self.mdf.channel_groups() {
            for channel in group.channels() {
                if let Some(name) = channel.name()? {
                    if name == channel_name {
                        let values = channel.values_as_f64()?;
                        let array = PyArray1::from_vec_bound(py, values);
                        return Ok(Some(array.into()));
                    }
                }
            }
        }
        Ok(None)
    }

    /// Get channel values by channel group name and channel name as numpy array.
    ///
    /// Returns None if either the group or channel is not found.
    fn get_channel_values_by_group_and_name<'py>(&self, py: Python<'py>, group_name: &str, channel_name: &str) -> PyResult<Option<PyObject>> {
        for group in self.mdf.channel_groups() {
            if let Some(gname) = group.name()? {
                if gname == group_name {
                    for channel in group.channels() {
                        if let Some(cname) = channel.name()? {
                            if cname == channel_name {
                                let values = channel.values_as_f64()?;
                                let array = PyArray1::from_vec_bound(py, values);
                                return Ok(Some(array.into()));
                            }
                        }
                    }
                    return Ok(None);
                }
            }
        }
        Ok(None)
    }

    /// Get channel data as a pandas Series with time/master channel as DatetimeIndex.
    ///
    /// Returns None if the channel is not found, otherwise returns a pandas Series.
    fn get_channel_as_series(&self, py: Python, channel_name: &str) -> PyResult<Option<PyObject>> {
        let pd = check_pandas_available(py)?;
        let start_time_ns = self.mdf.start_time_ns();

        for group in self.mdf.channel_groups() {
            let channels = group.channels();

            for (ch_idx, channel) in channels.iter().enumerate() {
                if let Some(name) = channel.name()? {
                    if name == channel_name {
                        let values = channel.values_as_f64()?;
                        let py_values = PyArray1::from_vec_bound(py, values);

                        let index: PyObject = if let Some(master_idx) = find_master_channel(&channels) {
                            if master_idx != ch_idx {
                                let master_values = channels[master_idx].values_as_f64()?;
                                let py_master = PyArray1::from_vec_bound(py, master_values);

                                if let Some(start_ns) = start_time_ns {
                                    // Vectorized datetime conversion using pandas
                                    let to_datetime = pd.getattr(py, "to_datetime")?;
                                    let to_timedelta = pd.getattr(py, "to_timedelta")?;
                                    let start_ts = to_datetime.call(
                                        py, (start_ns,),
                                        Some([("unit", "ns")].into_py_dict(py))
                                    )?;
                                    let deltas = to_timedelta.call(
                                        py, (py_master.clone(),),
                                        Some([("unit", "s")].into_py_dict(py))
                                    )?;
                                    match deltas.call_method1(py, "__add__", (start_ts,)) {
                                        Ok(datetime_index) => datetime_index,
                                        Err(_) => py_master.into(),
                                    }
                                } else {
                                    py_master.into()
                                }
                            } else {
                                py.None()
                            }
                        } else {
                            py.None()
                        };

                        let series_class = pd.getattr(py, "Series")?;
                        let series = if index.is_none(py) {
                            series_class.call1(py, (py_values,))?
                        } else {
                            series_class.call(py, (py_values,), Some([("index", index)].into_py_dict(py)))?
                        };

                        series.setattr(py, "name", channel_name)?;
                        return Ok(Some(series));
                    }
                }
            }
        }
        Ok(None)
    }

    /// Build a [`PyFileLayout`] describing every block in this file.
    ///
    /// Use this to inspect on-disk byte ranges, link chains and how data
    /// is stored relative to block definitions.
    fn file_layout(&self) -> PyResult<PyFileLayout> {
        let layout = self.mdf.file_layout()?;
        Ok(PyFileLayout { inner: layout })
    }
}

// Python wrapper for MdfWriter
#[pyclass(unsendable)]
pub struct PyMdfWriter {
    writer: Option<MdfWriter>,
    channel_groups: HashMap<String, String>, // Maps Python ID to Rust ID
    channels: HashMap<String, String>,       // Maps Python ID to Rust ID
    // Track the last channel added for each channel group (for automatic linking)
    last_channels: HashMap<String, String>,   // Maps channel group ID to last channel ID
    next_id: usize,
}

impl PyMdfWriter {
    fn add_channel_with_bits(&mut self, group_id: &str, name: &str, data_type: PyDataType, bit_count: u32) -> PyResult<String> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?;
            let prev_channel_id = self.last_channels.get(group_id)
                .and_then(|py_id| self.channels.get(py_id))
                .cloned();
            let rust_data_type = DataType::from(data_type);
            let ch_id = writer.add_channel(cg_id, prev_channel_id.as_ref().map(|s| s.as_str()), |ch| {
                ch.data_type = rust_data_type.clone();
                ch.name = Some(name.to_string());
                ch.bit_count = bit_count;
            })?;
            let py_id = format!("ch_{}", self.next_id);
            self.next_id += 1;
            self.channels.insert(py_id.clone(), ch_id);
            self.last_channels.insert(group_id.to_string(), py_id.clone());
            Ok(py_id)
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
}

#[pymethods]
impl PyMdfWriter {
    /// Create a new MdfWriter
    #[new]
    fn new(path: &str) -> PyResult<Self> {
        let writer = MdfWriter::new(path)?;
        Ok(PyMdfWriter {
            writer: Some(writer),
            channel_groups: HashMap::new(),
            channels: HashMap::new(),
            last_channels: HashMap::new(),
            next_id: 0,
        })
    }
    
    /// Initialize the MDF file
    fn init_mdf_file(&mut self) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            writer.init_mdf_file()?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Add a channel group
    fn add_channel_group(&mut self, name: Option<String>) -> PyResult<String> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = writer.add_channel_group(None, |_cg| {
                // Could set channel group properties here
            })?;
            
            let py_id = format!("cg_{}", self.next_id);
            self.next_id += 1;
            self.channel_groups.insert(py_id.clone(), cg_id);
            
            Ok(py_id)
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Add a channel to a channel group with automatic linking and bit count
    fn add_channel(&mut self,
                   group_id: &str,
                   name: &str,
                   data_type: PyDataType) -> PyResult<String> {
        let bits = {
            let rust_dt = DataType::from(data_type.clone());
            rust_dt.default_bits()
        };
        self.add_channel_with_bits(group_id, name, data_type, bits)
    }
    
    /// Add a time channel (float64, commonly used as master channel)
    fn add_time_channel(&mut self, group_id: &str, name: &str) -> PyResult<String> {
        let ch_id = self.add_channel_with_bits(group_id, name, PyDataType { name: "FloatLE".to_string(), value: 4 }, 64)?;
        self.set_time_channel(&ch_id)?;
        Ok(ch_id)
    }

    /// Add a float64 data channel (default for scientific data)
    fn add_float_channel(&mut self, group_id: &str, name: &str) -> PyResult<String> {
        self.add_channel_with_bits(group_id, name, PyDataType { name: "FloatLE".to_string(), value: 4 }, 64)
    }

    /// Add a float32 data channel
    fn add_float32_channel(&mut self, group_id: &str, name: &str) -> PyResult<String> {
        self.add_channel_with_bits(group_id, name, PyDataType { name: "FloatLE".to_string(), value: 4 }, 32)
    }

    /// Add an integer data channel (64-bit unsigned)
    fn add_int_channel(&mut self, group_id: &str, name: &str) -> PyResult<String> {
        self.add_channel_with_bits(group_id, name, PyDataType { name: "UnsignedIntegerLE".to_string(), value: 0 }, 64)
    }
    
    /// Set a channel as time/master channel
    fn set_time_channel(&mut self, channel_id: &str) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            let ch_id = self.channels.get(channel_id)
                .ok_or_else(|| MdfException::new_err("Channel not found"))?;
            writer.set_time_channel(ch_id)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Start data block for a channel group
    fn start_data_block(&mut self, group_id: &str) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?;
            writer.start_data_block_for_cg(cg_id, 0)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Write a record to a channel group
    fn write_record(&mut self, group_id: &str, values: Vec<PyDecodedValue>) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?;
            
            let rust_values: Vec<DecodedValue> = values.into_iter().map(DecodedValue::from).collect();
            writer.write_record(cg_id, &rust_values)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Finish data block for a channel group
    fn finish_data_block(&mut self, group_id: &str) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?;
            writer.finish_data_block(cg_id)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }
    
    /// Write multiple channels at once using numpy float64 arrays (one array per channel).
    ///
    /// `columns` must be a list of 1-D numpy float64 arrays all of the same length.
    /// This is significantly faster than calling `write_record` in a loop.
    fn write_columns_f64(&mut self, _py: Python<'_>, group_id: &str, columns: Vec<Bound<'_, PyAny>>) -> PyResult<()> {
        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?
                .clone();

            let arrays: Vec<PyReadonlyArray1<f64>> = columns.iter()
                .map(|c| c.extract::<PyReadonlyArray1<f64>>())
                .collect::<PyResult<Vec<_>>>()?;
            let slices: Vec<&[f64]> = arrays.iter()
                .map(|a| a.as_slice().map_err(|e| MdfException::new_err(format!("Array not contiguous: {}", e))))
                .collect::<PyResult<Vec<_>>>()?;

            writer.write_columns_f64(&cg_id, &slices)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }

    /// Write multiple channels at once using numpy arrays with explicit dtype strings.
    ///
    /// `columns` is a list of 1-D numpy arrays (all the same length).
    /// `dtypes` is a list of strings matching each column: `"f64"`, `"f32"`, `"u64"`, or `"i64"`.
    /// This is significantly faster than calling `write_record` in a loop.
    fn write_columns(&mut self, _py: Python<'_>, group_id: &str, columns: Vec<Bound<'_, PyAny>>, dtypes: Vec<String>) -> PyResult<()> {
        if columns.len() != dtypes.len() {
            return Err(MdfException::new_err(format!(
                "columns length ({}) must match dtypes length ({})",
                columns.len(), dtypes.len()
            )));
        }

        if let Some(ref mut writer) = self.writer {
            let cg_id = self.channel_groups.get(group_id)
                .ok_or_else(|| MdfException::new_err("Channel group not found"))?
                .clone();

            // We need to keep the extracted arrays alive for the duration of the call.
            // Use an enum to hold each typed array so lifetimes work out.
            enum OwnedArray<'py> {
                F64(PyReadonlyArray1<'py, f64>),
                F32(PyReadonlyArray1<'py, f32>),
                U64(PyReadonlyArray1<'py, u64>),
                I64(PyReadonlyArray1<'py, i64>),
            }

            let owned: Vec<OwnedArray<'_>> = columns.iter().zip(dtypes.iter())
                .map(|(col, dtype)| match dtype.as_str() {
                    "f64" => col.extract::<PyReadonlyArray1<f64>>().map(OwnedArray::F64),
                    "f32" => col.extract::<PyReadonlyArray1<f32>>().map(OwnedArray::F32),
                    "u64" => col.extract::<PyReadonlyArray1<u64>>().map(OwnedArray::U64),
                    "i64" => col.extract::<PyReadonlyArray1<i64>>().map(OwnedArray::I64),
                    other => Err(MdfException::new_err(format!(
                        "Unknown dtype '{}'; expected one of: f64, f32, u64, i64", other
                    ))),
                })
                .collect::<PyResult<Vec<_>>>()?;

            let column_data: Vec<ColumnData<'_>> = owned.iter()
                .map(|arr| match arr {
                    OwnedArray::F64(a) => a.as_slice()
                        .map(ColumnData::F64)
                        .map_err(|e| MdfException::new_err(format!("Array not contiguous: {}", e))),
                    OwnedArray::F32(a) => a.as_slice()
                        .map(ColumnData::F32)
                        .map_err(|e| MdfException::new_err(format!("Array not contiguous: {}", e))),
                    OwnedArray::U64(a) => a.as_slice()
                        .map(ColumnData::U64)
                        .map_err(|e| MdfException::new_err(format!("Array not contiguous: {}", e))),
                    OwnedArray::I64(a) => a.as_slice()
                        .map(ColumnData::I64)
                        .map_err(|e| MdfException::new_err(format!("Array not contiguous: {}", e))),
                })
                .collect::<PyResult<Vec<_>>>()?;

            writer.write_columns(&cg_id, &column_data)?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer has been finalized"))
        }
    }

    /// Finalize the writer and close the file
    fn finalize(&mut self) -> PyResult<()> {
        if let Some(writer) = self.writer.take() {
            writer.finalize()?;
            Ok(())
        } else {
            Err(MdfException::new_err("Writer already finalized"))
        }
    }
}

// Python wrapper for MdfIndex
#[pyclass]
pub struct PyMdfIndex {
    index: MdfIndex,
}

#[pymethods]
impl PyMdfIndex {
    /// Create index from MDF file
    #[staticmethod]
    fn from_file(path: &str) -> PyResult<Self> {
        let index = MdfIndex::from_file(path)?;
        Ok(PyMdfIndex { index })
    }
    
    /// Load index from JSON file
    #[staticmethod]
    fn load_from_file(path: &str) -> PyResult<Self> {
        let index = MdfIndex::load_from_file(path)?;
        Ok(PyMdfIndex { index })
    }
    
    /// Save index to JSON file
    fn save_to_file(&self, path: &str) -> PyResult<()> {
        self.index.save_to_file(path)?;
        Ok(())
    }
    
    /// List all channel groups
    fn list_channel_groups(&self) -> Vec<(usize, String, usize)> {
        self.index.list_channel_groups()
            .into_iter()
            .map(|(idx, name, count)| (idx, name.to_string(), count))
            .collect()
    }
    
    /// List channels in a specific group
    fn list_channels(&self, group_index: usize) -> Option<Vec<(usize, String, PyDataType)>> {
        self.index.list_channels(group_index)
            .map(|channels| {
                channels.into_iter()
                    .map(|(idx, name, data_type)| (idx, name.to_string(), PyDataType::from(data_type)))
                    .collect()
            })
    }
    
    /// Read channel values by index.
    ///
    /// Returns an error if indices are invalid.
    /// Option in Vec represents invalid/missing samples: None = invalid, Some = valid value.
    /// Returns a list of native Python values (float, int, str, bytes).
    fn read_channel_values(&self, py: Python, group_index: usize, channel_index: usize, file_path: &str) -> PyResult<Vec<Option<PyObject>>> {
        let mut reader = FileRangeReader::new(file_path)?;
        let values = self.index.read_channel_values(group_index, channel_index, &mut reader)?;
        Ok(values.into_iter().map(|opt_val| {
            opt_val.map(|dv| decoded_value_to_pyobject(dv, py))
        }).collect())
    }
    
    /// Read channel values by name.
    ///
    /// Returns an error if the channel is not found.
    /// Option in Vec represents invalid/missing samples: None = invalid, Some = valid value.
    /// Returns a list of native Python values (float, int, str, bytes).
    fn read_channel_values_by_name(&self, py: Python, channel_name: &str, file_path: &str) -> PyResult<Vec<Option<PyObject>>> {
        let mut reader = FileRangeReader::new(file_path)?;
        let values = self.index.read_channel_values_by_name(channel_name, &mut reader)?;
        Ok(values.into_iter().map(|opt_val| {
            opt_val.map(|dv| decoded_value_to_pyobject(dv, py))
        }).collect())
    }
    
    /// Fast path: read channel values as f64 list by index.
    ///
    /// Significantly faster than `read_channel_values` for numeric channels.
    /// Uses zero-copy mmap access and bypasses DecodedValue enum boxing.
    /// Invalid samples are represented as float('nan').
    fn read_channel_values_as_f64(&self, group_index: usize, channel_index: usize, file_path: &str) -> PyResult<Vec<f64>> {
        let file = std::fs::File::open(file_path).map_err(|e| MdfError::IOError(e))?;
        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| MdfError::IOError(e))?;
        Ok(self.index.read_channel_values_from_slice_as_f64(group_index, channel_index, &mmap)?)
    }

    /// Fast path: read channel values as f64 list by name.
    ///
    /// Significantly faster than `read_channel_values_by_name` for numeric channels.
    /// Uses zero-copy mmap access and bypasses DecodedValue enum boxing.
    /// Invalid samples are represented as float('nan').
    fn read_channel_values_by_name_as_f64(&self, channel_name: &str, file_path: &str) -> PyResult<Vec<f64>> {
        let (group_index, channel_index) = self.index.find_channel_by_name_global(channel_name)
            .ok_or_else(|| MdfError::BlockSerializationError(
                format!("Channel '{}' not found", channel_name)
            ))?;
        let file = std::fs::File::open(file_path).map_err(|e| MdfError::IOError(e))?;
        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| MdfError::IOError(e))?;
        Ok(self.index.read_channel_values_from_slice_as_f64(group_index, channel_index, &mmap)?)
    }

    /// Find channel by name
    fn find_channel_by_name(&self, channel_name: &str) -> Option<(usize, usize)> {
        self.index.find_channel_by_name_global(channel_name)
    }
    
    /// Get byte ranges for a channel
    fn get_channel_byte_ranges(&self, group_index: usize, channel_index: usize) -> PyResult<Vec<(u64, u64)>> {
        Ok(self.index.get_channel_byte_ranges(group_index, channel_index)?)
    }
    
    /// Get byte ranges for a specific record range of a channel
    fn get_channel_byte_ranges_for_records(&self, group_index: usize, channel_index: usize, start_record: u64, record_count: u64) -> PyResult<Vec<(u64, u64)>> {
        Ok(self.index.get_channel_byte_ranges_for_records(group_index, channel_index, start_record, record_count)?)
    }
    
    /// Get byte range summary for a channel (total bytes, number of ranges)
    fn get_channel_byte_summary(&self, group_index: usize, channel_index: usize) -> PyResult<(u64, usize)> {
        Ok(self.index.get_channel_byte_summary(group_index, channel_index)?)
    }
    
    /// Get byte ranges for a channel by name
    fn get_channel_byte_ranges_by_name(&self, channel_name: &str) -> PyResult<Vec<(u64, u64)>> {
        Ok(self.index.get_channel_byte_ranges_by_name(channel_name)?)
    }
    
    /// Get channel info by name (returns group_index, channel_index, and channel info)
    fn get_channel_info_by_name(&self, channel_name: &str) -> Option<(usize, usize, PyChannelInfo)> {
        self.index.get_channel_info_by_name(channel_name).map(|(group_idx, channel_idx, channel)| {
            let info = PyChannelInfo {
                name: channel.name.clone(),
                unit: channel.unit.clone(),
                comment: None, // IndexedChannel doesn't store comment
                data_type: PyDataType::from(&channel.data_type),
                bit_count: channel.bit_count,
            };
            (group_idx, channel_idx, info)
        })
    }
    
    /// Find all channels with a given name across all groups
    fn find_all_channels_by_name(&self, channel_name: &str) -> Vec<(usize, usize)> {
        self.index.find_all_channels_by_name(channel_name)
    }
    
    /// Get file size from the index
    fn get_file_size(&self) -> u64 {
        self.index.file_size
    }
    
    /// Check if the index has resolved conversion data (enhanced index)
    fn has_resolved_conversions(&self) -> bool {
        // Check if any channel has resolved conversion data
        for group in &self.index.channel_groups {
            for channel in &group.channels {
                if let Some(conversion) = &channel.conversion {
                    if conversion.resolved_texts.is_some() || conversion.resolved_conversions.is_some() {
                        return true;
                    }
                }
            }
        }
        false
    }
    
    /// Get conversion info for a specific channel
    fn get_conversion_info(&self, group_index: usize, channel_index: usize) -> PyResult<Option<HashMap<String, PyObject>>> {
        use pyo3::Python;

        let group = self.index.channel_groups.get(group_index)
            .ok_or_else(|| MdfException::new_err("Invalid group index"))?;

        let channel = group.channels.get(channel_index)
            .ok_or_else(|| MdfException::new_err("Invalid channel index"))?;

        if let Some(conversion) = &channel.conversion {
            Python::with_gil(|py| {
                let mut info = HashMap::new();

                info.insert("conversion_type".to_string(), format!("{:?}", conversion.cc_type).to_object(py));
                info.insert("precision".to_string(), conversion.cc_precision.to_object(py));
                info.insert("flags".to_string(), conversion.cc_flags.to_object(py));
                info.insert("values_count".to_string(), conversion.cc_val_count.to_object(py));
                info.insert("values".to_string(), conversion.cc_val.to_object(py));

                // Include resolved data info if available
                if let Some(resolved_texts) = &conversion.resolved_texts {
                    let texts: HashMap<usize, String> = resolved_texts.clone();
                    info.insert("resolved_texts".to_string(), texts.to_object(py));
                }

                if let Some(_) = &conversion.resolved_conversions {
                    info.insert("has_resolved_conversions".to_string(), true.to_object(py));
                }

                if let Some(formula) = &conversion.formula {
                    info.insert("formula".to_string(), formula.to_object(py));
                }

                Ok(Some(info))
            })
        } else {
            Ok(None)
        }
    }

    /// Read channel values by channel group name and channel name.
    ///
    /// This method looks up the channel group by name first, then finds the channel
    /// within that specific group. Returns an error if either the group or channel is not found.
    /// Option in Vec represents invalid/missing samples: None = invalid, Some = valid value.
    /// Returns a list of native Python values (float, int, str, bytes).
    fn read_channel_values_by_group_and_name(&self, py: Python, group_name: &str, channel_name: &str, file_path: &str) -> PyResult<Vec<Option<PyObject>>> {
        // Find the group by name
        let mut group_index = None;
        for (idx, group) in self.index.channel_groups.iter().enumerate() {
            if let Some(ref gname) = group.name {
                if gname == group_name {
                    group_index = Some(idx);
                    break;
                }
            }
        }

        let group_idx = group_index.ok_or_else(||
            MdfException::new_err(format!("Channel group '{}' not found", group_name))
        )?;

        // Find the channel by name within the group
        let group = &self.index.channel_groups[group_idx];
        let mut channel_index = None;
        for (idx, channel) in group.channels.iter().enumerate() {
            if let Some(ref cname) = channel.name {
                if cname == channel_name {
                    channel_index = Some(idx);
                    break;
                }
            }
        }

        let channel_idx = channel_index.ok_or_else(||
            MdfException::new_err(format!("Channel '{}' not found in group '{}'", channel_name, group_name))
        )?;

        // Read the channel values using the found indices
        let mut reader = FileRangeReader::new(file_path)?;
        let values = self.index.read_channel_values(group_idx, channel_idx, &mut reader)?;
        Ok(values.into_iter().map(|opt_val| {
            opt_val.map(|dv| decoded_value_to_pyobject(dv, py))
        }).collect())
    }

    /// Read channel data as a pandas Series with time/master channel as DatetimeIndex.
    ///
    /// This method reads a channel's values from the index and returns them as a pandas Series
    /// with the time/master channel values converted to absolute timestamps as a DatetimeIndex.
    /// The timestamps are created by adding the relative time values to the MDF start time
    /// stored in the index. If no master channel is found, or if the queried channel IS the
    /// master channel, a default integer index is used. If the MDF file has no start time,
    /// falls back to numeric index.
    ///
    /// Requires pandas to be installed.
    ///
    /// # Arguments
    /// * `channel_name` - Name of the channel to read
    /// * `file_path` - Path to the MDF file
    ///
    /// # Returns
    /// Returns an error if the channel is not found, otherwise returns a pandas Series.
    ///
    /// # Errors
    /// Returns an error if:
    /// - pandas is not installed
    /// - the channel is not found
    /// - the master channel has a different number of values than the data channel
    fn read_channel_as_series(&self, py: Python, channel_name: &str, file_path: &str) -> PyResult<PyObject> {
        // Check if pandas is available
        let pd = check_pandas_available(py)?;

        // Get the MDF start time from the index for datetime conversion
        let start_time_ns = self.index.start_time_ns;

        // Find the channel
        let (group_idx, channel_idx) = self.find_channel_by_name(channel_name)
            .ok_or_else(|| MdfException::new_err(format!("Channel '{}' not found", channel_name)))?;

        // Read the channel values
        let mut reader = FileRangeReader::new(file_path)?;
        let values = self.index.read_channel_values(group_idx, channel_idx, &mut reader)?;
        let py_values: Vec<PyObject> = values.into_iter().map(|opt_val| {
            opt_val.map(|dv| decoded_value_to_pyobject(dv, py)).unwrap_or_else(|| py.None())
        }).collect();

        // Find the master/time channel for this group
        let group = &self.index.channel_groups[group_idx];
        let index: PyObject = if let Some(master_idx) = find_master_channel_indexed(&group.channels) {
            if master_idx != channel_idx {
                // Use the master channel values as index
                let master_values = self.index.read_channel_values(group_idx, master_idx, &mut reader)?;
                let py_master_values: Vec<PyObject> = master_values.into_iter().map(|opt_val| {
                    opt_val.map(|dv| decoded_value_to_pyobject(dv, py)).unwrap_or_else(|| py.None())
                }).collect();

                // Validate that lengths match
                if py_master_values.len() != py_values.len() {
                    return Err(MdfException::new_err(format!(
                        "Master channel length ({}) does not match data channel length ({}) for channel '{}'",
                        py_master_values.len(), py_values.len(), channel_name
                    )));
                }

                // Try to convert to DatetimeIndex if we have a start time
                if let Some(start_ns) = start_time_ns {
                    // Attempt to create DatetimeIndex from absolute timestamps
                    match create_datetime_index(py, &pd, &py_master_values, start_ns) {
                        Ok(datetime_index) => datetime_index,
                        Err(_) => {
                            // Fall back to numeric index if datetime conversion fails
                            py_master_values.to_object(py)
                        }
                    }
                } else {
                    // No start time, use numeric index
                    py_master_values.to_object(py)
                }
            } else {
                // This channel IS the master channel, use default index
                py.None()
            }
        } else {
            // No master channel found, use default index
            py.None()
        };

        // Create pandas Series
        let series_class = pd.getattr(py, "Series")?;
        let series = if index.is_none(py) {
            // No index specified, pandas will use default integer index
            series_class.call1(py, (py_values,))?
        } else {
            // Use the master channel values as index
            series_class.call(py, (py_values,), Some([("index", index)].into_py_dict(py)))?
        };

        // Set the series name to the channel name
        series.setattr(py, "name", channel_name)?;

        Ok(series)
    }
}

// ---------------------------------------------------------------------------
// Block layout visualization
// ---------------------------------------------------------------------------

/// A named link inside a block (wraps [`LinkInfo`]).
#[pyclass]
#[derive(Clone)]
pub struct PyLinkInfo {
    #[pyo3(get)]
    pub name: String,
    #[pyo3(get)]
    pub target: u64,
    #[pyo3(get)]
    pub target_type: Option<String>,
}

#[pymethods]
impl PyLinkInfo {
    fn __repr__(&self) -> String {
        if self.target == 0 {
            format!("Link({} -> null)", self.name)
        } else {
            format!(
                "Link({} -> 0x{:010x} [{}])",
                self.name,
                self.target,
                self.target_type.as_deref().unwrap_or("?")
            )
        }
    }
}

impl From<LinkInfo> for PyLinkInfo {
    fn from(l: LinkInfo) -> Self {
        PyLinkInfo {
            name: l.name,
            target: l.target,
            target_type: l.target_type,
        }
    }
}

/// A block visited while walking the file (wraps [`BlockInfo`]).
#[pyclass]
#[derive(Clone)]
pub struct PyBlockInfo {
    #[pyo3(get)]
    pub offset: u64,
    #[pyo3(get)]
    pub end_offset: u64,
    #[pyo3(get)]
    pub size: u64,
    #[pyo3(get)]
    pub block_type: String,
    #[pyo3(get)]
    pub description: String,
    #[pyo3(get)]
    pub links: Vec<PyLinkInfo>,
    #[pyo3(get)]
    pub extra: Option<String>,
}

#[pymethods]
impl PyBlockInfo {
    fn __repr__(&self) -> String {
        format!(
            "Block({} @ 0x{:010x}..0x{:010x}, size={}, {})",
            self.block_type, self.offset, self.end_offset, self.size, self.description
        )
    }
}

impl From<BlockInfo> for PyBlockInfo {
    fn from(b: BlockInfo) -> Self {
        PyBlockInfo {
            offset: b.offset,
            end_offset: b.end_offset,
            size: b.size,
            block_type: b.block_type,
            description: b.description,
            links: b.links.into_iter().map(Into::into).collect(),
            extra: b.extra,
        }
    }
}

/// An unreferenced range of bytes in the file (wraps [`GapInfo`]).
#[pyclass]
#[derive(Clone)]
pub struct PyGapInfo {
    #[pyo3(get)]
    pub start: u64,
    #[pyo3(get)]
    pub end: u64,
    #[pyo3(get)]
    pub size: u64,
}

#[pymethods]
impl PyGapInfo {
    fn __repr__(&self) -> String {
        format!(
            "Gap(0x{:010x}..0x{:010x}, size={})",
            self.start, self.end, self.size
        )
    }
}

impl From<GapInfo> for PyGapInfo {
    fn from(g: GapInfo) -> Self {
        PyGapInfo {
            start: g.start,
            end: g.end,
            size: g.size,
        }
    }
}

/// Full layout of an MDF file (wraps [`FileLayout`]).
#[pyclass]
pub struct PyFileLayout {
    pub(crate) inner: FileLayout,
}

#[pymethods]
impl PyFileLayout {
    /// Build a layout by parsing an MDF file on disk.
    #[staticmethod]
    fn from_file(path: &str) -> PyResult<Self> {
        let inner = FileLayout::from_file(path)?;
        Ok(PyFileLayout { inner })
    }

    /// Total size of the file in bytes.
    #[getter]
    fn file_size(&self) -> u64 {
        self.inner.file_size
    }

    /// List of all discovered blocks, sorted by offset.
    #[getter]
    fn blocks(&self) -> Vec<PyBlockInfo> {
        self.inner.blocks.iter().cloned().map(Into::into).collect()
    }

    /// Byte ranges not covered by any visited block.
    #[getter]
    fn gaps(&self) -> Vec<PyGapInfo> {
        self.inner.gaps.iter().cloned().map(Into::into).collect()
    }

    /// Render the layout as a flat, sorted listing with link details.
    fn to_text(&self) -> String {
        self.inner.to_text()
    }

    /// Render the link graph as an indented tree starting at the ID block.
    fn to_tree(&self) -> String {
        self.inner.to_tree()
    }

    /// Serialize the layout to a pretty JSON string.
    fn to_json(&self) -> PyResult<String> {
        Ok(self.inner.to_json()?)
    }

    /// Write the flat text listing to `path`.
    fn write_text_to_file(&self, path: &str) -> PyResult<()> {
        self.inner.write_text_to_file(path)?;
        Ok(())
    }

    /// Write the indented tree view to `path`.
    fn write_tree_to_file(&self, path: &str) -> PyResult<()> {
        self.inner.write_tree_to_file(path)?;
        Ok(())
    }

    /// Write the JSON representation to `path`.
    fn write_json_to_file(&self, path: &str) -> PyResult<()> {
        self.inner.write_json_to_file(path)?;
        Ok(())
    }

    fn __repr__(&self) -> String {
        format!(
            "FileLayout(file_size={}, blocks={}, gaps={})",
            self.inner.file_size,
            self.inner.blocks.len(),
            self.inner.gaps.len()
        )
    }
}

/// Build a [`PyFileLayout`] directly from a file path (free function helper).
#[pyfunction]
fn file_layout_from_file(path: &str) -> PyResult<PyFileLayout> {
    PyFileLayout::from_file(path)
}

// Helper functions

/// Create a float decoded value
#[pyfunction]
fn create_float_value(value: f64) -> PyDecodedValue {
    PyDecodedValue::Float { value }
}

/// Create an unsigned integer decoded value  
#[pyfunction]
fn create_uint_value(value: u64) -> PyDecodedValue {
    PyDecodedValue::UnsignedInteger { value }
}

/// Create a signed integer decoded value
#[pyfunction]
fn create_int_value(value: i64) -> PyDecodedValue {
    PyDecodedValue::SignedInteger { value }
}

/// Create a string decoded value
#[pyfunction]
fn create_string_value(value: String) -> PyDecodedValue {
    PyDecodedValue::String { value }
}

/// Create data types
#[pyfunction]
fn create_data_type_uint_le() -> PyDataType {
    PyDataType { name: "UnsignedIntegerLE".to_string(), value: 0 }
}

#[pyfunction]
fn create_data_type_float_le() -> PyDataType {
    PyDataType { name: "FloatLE".to_string(), value: 4 }
}

#[pyfunction]
fn create_data_type_string_utf8() -> PyDataType {
    PyDataType { name: "StringUtf8".to_string(), value: 7 }
}

/// The main Python module initialization function
pub fn init_mf4_rs_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add("MdfException", m.py().get_type_bound::<MdfException>())?;
    
    // Classes
    m.add_class::<PyMDF>()?;
    m.add_class::<PyMdfWriter>()?;
    m.add_class::<PyMdfIndex>()?;
    m.add_class::<PyChannelGroupInfo>()?;
    m.add_class::<PyChannelInfo>()?;
    m.add_class::<PyDecodedValue>()?;
    m.add_class::<PyDataType>()?;
    m.add_class::<PyFileLayout>()?;
    m.add_class::<PyBlockInfo>()?;
    m.add_class::<PyLinkInfo>()?;
    m.add_class::<PyGapInfo>()?;

    // Helper functions
    m.add_function(wrap_pyfunction!(create_float_value, m)?)?;
    m.add_function(wrap_pyfunction!(create_uint_value, m)?)?;
    m.add_function(wrap_pyfunction!(create_int_value, m)?)?;
    m.add_function(wrap_pyfunction!(create_string_value, m)?)?;
    m.add_function(wrap_pyfunction!(create_data_type_uint_le, m)?)?;
    m.add_function(wrap_pyfunction!(create_data_type_float_le, m)?)?;
    m.add_function(wrap_pyfunction!(create_data_type_string_utf8, m)?)?;
    m.add_function(wrap_pyfunction!(file_layout_from_file, m)?)?;

    Ok(())
}
