use crate::component::GridType;
use crate::errors::RSCMResult;
use crate::spatial::{
    FourBoxGrid, FourBoxRegion, HemisphericGrid, HemisphericRegion, ScalarGrid, ScalarRegion,
    SpatialGrid,
};
use crate::timeseries::{FloatValue, GridTimeseries, Time, Timeseries};
use crate::timeseries_collection::{TimeseriesData, TimeseriesItem, VariableType};
use ndarray::ArrayView1;
use num::Float;
use std::collections::HashMap;

/// A zero-cost view into a scalar timeseries at a specific time index.
///
/// `TimeseriesWindow` provides efficient access to current, historical, and interpolated
/// values without copying data. This is the primary way components access their input
/// variables.
///
/// # Timestep Access Semantics
///
/// Components must explicitly choose which timestep index to read from:
/// - [`at_start()`](Self::at_start) - Value at index N (start of timestep). Use for:
///   - State variables (your own previous state)
///   - Exogenous inputs (external forcing data)
/// - [`at_end()`](Self::at_end) - Value at index N+1 (written this timestep). Use for:
///   - Upstream component outputs (values written before your component ran)
///   - Aggregation (combining outputs from multiple components)
///
/// # Examples
///
/// ```ignore
/// fn solve(&self, inputs: MyComponentInputs) -> MyComponentOutputs {
///     // Read exogenous input at start of timestep
///     let emissions = inputs.emissions_co2.at_start();
///
///     // Read previous value for derivative calculation
///     let previous = inputs.emissions_co2.previous();
///     let derivative = (emissions - previous.unwrap_or(emissions)) / dt;
///
///     // Access historical values
///     let last_5 = inputs.emissions_co2.last_n(5);
///     // ...
/// }
/// ```
#[derive(Debug)]
pub struct TimeseriesWindow<'a> {
    timeseries: &'a Timeseries<FloatValue>,
    current_index: usize,
    current_time: Time,
}

impl<'a> TimeseriesWindow<'a> {
    /// Create a new TimeseriesWindow from a scalar timeseries.
    ///
    /// # Arguments
    ///
    /// * `timeseries` - The underlying scalar timeseries
    /// * `current_index` - Index of the current timestep
    /// * `current_time` - Time value at the current timestep
    pub fn new(
        timeseries: &'a Timeseries<FloatValue>,
        current_index: usize,
        current_time: Time,
    ) -> Self {
        Self {
            timeseries,
            current_index,
            current_time,
        }
    }

    /// Get the value at the start of the timestep (index N).
    ///
    /// This is the primary accessor for reading input values. The "start of timestep"
    /// refers to the state before any components have executed during this timestep.
    ///
    /// # When to use `at_start()`
    ///
    /// - **State variables**: Reading your own component's state from the previous solve
    ///   (e.g., temperature at the beginning of the timestep before you update it)
    /// - **Exogenous inputs**: External forcing data that was pre-populated before the
    ///   model run (e.g., emissions scenarios, solar irradiance)
    /// - **Any input where you need the value at index N**
    ///
    /// # Execution order context
    ///
    /// Components execute in dependency order. When your component runs:
    /// - Index N contains values from before this timestep started
    /// - Index N+1 may contain values written by upstream components (use [`at_end()`])
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn solve(&self, t_current: Time, t_next: Time, input_state: &InputState) -> RSCMResult<OutputState> {
    ///     let inputs = MyComponentInputs::from_input_state(input_state);
    ///
    ///     // Read state variable (your own previous output)
    ///     let prev_temperature = inputs.temperature.at_start();
    ///
    ///     // Read exogenous forcing
    ///     let emissions = inputs.emissions.at_start();
    ///
    ///     // Compute new state
    ///     let new_temperature = prev_temperature + emissions * self.sensitivity;
    ///     // ...
    /// }
    /// ```
    ///
    /// [`at_end()`]: TimeseriesWindow::at_end
    pub fn at_start(&self) -> FloatValue {
        self.timeseries
            .at(self.current_index, ScalarRegion::Global)
            .expect("Current index out of bounds")
    }

    /// Get the value at the end of the timestep (index N+1), if available.
    ///
    /// This accessor reads values that were written during the current timestep by
    /// components that executed before you in the dependency order.
    ///
    /// # When to use `at_end()`
    ///
    /// - **Upstream component outputs**: When you depend on another component's output
    ///   from this timestep (they ran before you and wrote to index N+1)
    /// - **Aggregation**: Combining outputs from multiple components that all wrote
    ///   during the current timestep
    ///
    /// # Returns
    ///
    /// - `Some(value)` if index N+1 exists in the timeseries
    /// - `None` if at the last timestep (index N+1 is out of bounds)
    ///
    /// # Execution order context
    ///
    /// The model solves components in dependency order. Upstream components write their
    /// outputs to index N+1 before downstream components run. This method lets you read
    /// those freshly-written values.
    ///
    /// # Example
    ///
    /// ```ignore
    /// fn solve(&self, t_current: Time, t_next: Time, input_state: &InputState) -> RSCMResult<OutputState> {
    ///     let inputs = MyComponentInputs::from_input_state(input_state);
    ///
    ///     // Read upstream component output (written this timestep)
    ///     // Fall back to start value if at final timestep
    ///     let erf = inputs.effective_radiative_forcing
    ///         .at_end()
    ///         .unwrap_or_else(|| inputs.effective_radiative_forcing.at_start());
    ///
    ///     // Use the forcing to compute temperature response
    ///     let temp_change = erf * self.climate_sensitivity;
    ///     // ...
    /// }
    /// ```
    ///
    /// [`at_start()`]: TimeseriesWindow::at_start
    pub fn at_end(&self) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at(next_index, ScalarRegion::Global)
        }
    }

    /// Get the value at the previous timestep, if available.
    ///
    /// Returns `None` if at the first timestep (no previous value exists).
    pub fn previous(&self) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            self.timeseries
                .at(self.current_index - 1, ScalarRegion::Global)
        }
    }

    /// Get the value at a relative offset from the current timestep.
    ///
    /// Positive offsets look forward in time, negative offsets look backward.
    /// Returns `None` if the resulting index is out of bounds.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let prev = window.at_offset(-1);  // Same as previous()
    /// let two_back = window.at_offset(-2);
    /// let next = window.at_offset(1);   // Future value (if available)
    /// ```
    pub fn at_offset(&self, offset: isize) -> Option<FloatValue> {
        let index = self.current_index as isize + offset;
        if index < 0 || index as usize >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at(index as usize, ScalarRegion::Global)
        }
    }

    /// Get the last N values as an array view, ending at the current timestep.
    ///
    /// This is useful for computing moving averages, derivatives, or any operation
    /// that needs historical context.
    ///
    /// # Panics
    ///
    /// Panics if `n` is greater than `current_index + 1` (not enough history).
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let last_5 = window.last_n(5);
    /// let avg = last_5.mean().unwrap();
    /// ```
    pub fn last_n(&self, n: usize) -> ArrayView1<'_, FloatValue> {
        assert!(
            n <= self.current_index + 1,
            "Cannot get {} values when only {} available",
            n,
            self.current_index + 1
        );
        let start = self.current_index + 1 - n;
        let end = self.current_index + 1;
        // Get the values column (shape: [time, 1]) and slice the time dimension
        self.timeseries.values().slice(ndarray::s![start..end, 0])
    }

    /// Interpolate the value at an arbitrary time point.
    ///
    /// Uses the timeseries's interpolation strategy to compute the value.
    /// This is useful for sub-timestep calculations or when comparing with
    /// observational data at non-model times.
    pub fn interpolate(&self, t: Time) -> RSCMResult<FloatValue> {
        self.timeseries.at_time(t, ScalarRegion::Global)
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        self.current_time
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        self.current_index
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        self.timeseries.len()
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        self.timeseries.is_empty()
    }
}

// =============================================================================
// Grid-Aggregating Scalar Windows
// =============================================================================

/// A transformation context for grid aggregation.
///
/// This holds the information needed to transform grid data to scalar values
/// during read operations.
#[derive(Debug, Clone)]
pub struct ReadTransformInfo {
    /// The source grid type (finer resolution data)
    pub source_grid: GridType,
    /// The weights to use for aggregation (from Model's grid_weights)
    /// If None, use the grid's default weights
    pub weights: Option<Vec<f64>>,
}

/// A scalar window that aggregates from a FourBox timeseries.
///
/// This provides the same API as `TimeseriesWindow` but reads from a FourBox
/// timeseries and aggregates to a scalar value on each access.
#[derive(Debug)]
pub struct AggregatingFourBoxWindow<'a> {
    timeseries: &'a GridTimeseries<FloatValue, FourBoxGrid>,
    current_index: usize,
    current_time: Time,
    weights: Option<[f64; 4]>,
}

impl<'a> AggregatingFourBoxWindow<'a> {
    pub fn new(
        timeseries: &'a GridTimeseries<FloatValue, FourBoxGrid>,
        current_index: usize,
        current_time: Time,
        weights: Option<Vec<f64>>,
    ) -> Self {
        let weights = weights.map(|w| {
            let arr: [f64; 4] = w
                .as_slice()
                .try_into()
                .expect("FourBox weights must have 4 elements");
            arr
        });
        Self {
            timeseries,
            current_index,
            current_time,
            weights,
        }
    }

    fn aggregate(&self, values: &[FloatValue]) -> FloatValue {
        match &self.weights {
            Some(weights) => {
                let mut sum = 0.0;
                for (v, w) in values.iter().zip(weights.iter()) {
                    if !v.is_nan() {
                        sum += v * w;
                    }
                }
                sum
            }
            None => self.timeseries.grid().aggregate_global(values),
        }
    }

    /// Get the aggregated scalar value at the start of the timestep (index N).
    pub fn at_start(&self) -> FloatValue {
        let values = self
            .timeseries
            .at_time_index(self.current_index)
            .expect("Current index out of bounds");
        self.aggregate(&values)
    }

    /// Get the aggregated scalar value at the end of the timestep (index N+1), if available.
    pub fn at_end(&self) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            let values = self
                .timeseries
                .at_time_index(next_index)
                .expect("Next index out of bounds");
            Some(self.aggregate(&values))
        }
    }

    /// Get the aggregated value at the previous timestep.
    pub fn previous(&self) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            let values = self
                .timeseries
                .at_time_index(self.current_index - 1)
                .expect("Previous index out of bounds");
            Some(self.aggregate(&values))
        }
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        self.current_time
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        self.current_index
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        self.timeseries.len()
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        self.timeseries.is_empty()
    }
}

/// A scalar window that aggregates from a Hemispheric timeseries.
#[derive(Debug)]
pub struct AggregatingHemisphericWindow<'a> {
    timeseries: &'a GridTimeseries<FloatValue, HemisphericGrid>,
    current_index: usize,
    current_time: Time,
    weights: Option<[f64; 2]>,
}

impl<'a> AggregatingHemisphericWindow<'a> {
    pub fn new(
        timeseries: &'a GridTimeseries<FloatValue, HemisphericGrid>,
        current_index: usize,
        current_time: Time,
        weights: Option<Vec<f64>>,
    ) -> Self {
        let weights = weights.map(|w| {
            let arr: [f64; 2] = w
                .as_slice()
                .try_into()
                .expect("Hemispheric weights must have 2 elements");
            arr
        });
        Self {
            timeseries,
            current_index,
            current_time,
            weights,
        }
    }

    fn aggregate(&self, values: &[FloatValue]) -> FloatValue {
        match &self.weights {
            Some(weights) => {
                let mut sum = 0.0;
                for (v, w) in values.iter().zip(weights.iter()) {
                    if !v.is_nan() {
                        sum += v * w;
                    }
                }
                sum
            }
            None => self.timeseries.grid().aggregate_global(values),
        }
    }

    /// Get the aggregated scalar value at the start of the timestep (index N).
    pub fn at_start(&self) -> FloatValue {
        let values = self
            .timeseries
            .at_time_index(self.current_index)
            .expect("Current index out of bounds");
        self.aggregate(&values)
    }

    /// Get the aggregated scalar value at the end of the timestep (index N+1), if available.
    pub fn at_end(&self) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            let values = self
                .timeseries
                .at_time_index(next_index)
                .expect("Next index out of bounds");
            Some(self.aggregate(&values))
        }
    }

    /// Get the aggregated value at the previous timestep.
    pub fn previous(&self) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            let values = self
                .timeseries
                .at_time_index(self.current_index - 1)
                .expect("Previous index out of bounds");
            Some(self.aggregate(&values))
        }
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        self.current_time
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        self.current_index
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        self.timeseries.len()
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        self.timeseries.is_empty()
    }
}

/// A unified scalar window that can be either direct or aggregating.
///
/// This enum allows `InputState::get_scalar_window()` to return the same type
/// regardless of whether the underlying data is scalar or needs aggregation.
#[derive(Debug)]
pub enum ScalarWindow<'a> {
    /// Direct access to a scalar timeseries
    Direct(TimeseriesWindow<'a>),
    /// Aggregating access to a FourBox timeseries
    FromFourBox(AggregatingFourBoxWindow<'a>),
    /// Aggregating access to a Hemispheric timeseries
    FromHemispheric(AggregatingHemisphericWindow<'a>),
}

impl<'a> ScalarWindow<'a> {
    /// Get the scalar value at the start of the timestep (index N).
    pub fn at_start(&self) -> FloatValue {
        match self {
            ScalarWindow::Direct(w) => w.at_start(),
            ScalarWindow::FromFourBox(w) => w.at_start(),
            ScalarWindow::FromHemispheric(w) => w.at_start(),
        }
    }

    /// Get the scalar value at the end of the timestep (index N+1), if available.
    pub fn at_end(&self) -> Option<FloatValue> {
        match self {
            ScalarWindow::Direct(w) => w.at_end(),
            ScalarWindow::FromFourBox(w) => w.at_end(),
            ScalarWindow::FromHemispheric(w) => w.at_end(),
        }
    }

    /// Get the value at the previous timestep, if available.
    pub fn previous(&self) -> Option<FloatValue> {
        match self {
            ScalarWindow::Direct(w) => w.previous(),
            ScalarWindow::FromFourBox(w) => w.previous(),
            ScalarWindow::FromHemispheric(w) => w.previous(),
        }
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        match self {
            ScalarWindow::Direct(w) => w.time(),
            ScalarWindow::FromFourBox(w) => w.time(),
            ScalarWindow::FromHemispheric(w) => w.time(),
        }
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        match self {
            ScalarWindow::Direct(w) => w.index(),
            ScalarWindow::FromFourBox(w) => w.index(),
            ScalarWindow::FromHemispheric(w) => w.index(),
        }
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        match self {
            ScalarWindow::Direct(w) => w.len(),
            ScalarWindow::FromFourBox(w) => w.len(),
            ScalarWindow::FromHemispheric(w) => w.len(),
        }
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        match self {
            ScalarWindow::Direct(w) => w.is_empty(),
            ScalarWindow::FromFourBox(w) => w.is_empty(),
            ScalarWindow::FromHemispheric(w) => w.is_empty(),
        }
    }
}

/// A scalar window that aggregates from a Hemispheric timeseries to Hemispheric output.
///
/// This is used when reading a FourBox timeseries but the component wants Hemispheric data.
#[derive(Debug)]
pub struct AggregatingFourBoxToHemisphericWindow<'a> {
    timeseries: &'a GridTimeseries<FloatValue, FourBoxGrid>,
    current_index: usize,
    current_time: Time,
}

impl<'a> AggregatingFourBoxToHemisphericWindow<'a> {
    pub fn new(
        timeseries: &'a GridTimeseries<FloatValue, FourBoxGrid>,
        current_index: usize,
        current_time: Time,
    ) -> Self {
        Self {
            timeseries,
            current_index,
            current_time,
        }
    }

    fn aggregate_to_hemispheric(&self, values: &[FloatValue]) -> [FloatValue; 2] {
        // FourBox: [NorthernOcean, NorthernLand, SouthernOcean, SouthernLand]
        // Hemispheric: [Northern, Southern]
        // Average ocean+land for each hemisphere
        let northern = (values[0] + values[1]) / 2.0;
        let southern = (values[2] + values[3]) / 2.0;
        [northern, southern]
    }

    /// Get all regional values at the start of the timestep.
    pub fn at_start_all(&self) -> Vec<FloatValue> {
        let values = self
            .timeseries
            .at_time_index(self.current_index)
            .expect("Current index out of bounds");
        self.aggregate_to_hemispheric(&values).to_vec()
    }

    /// Get all regional values at the end of the timestep.
    pub fn at_end_all(&self) -> Option<Vec<FloatValue>> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            let values = self
                .timeseries
                .at_time_index(next_index)
                .expect("Next index out of bounds");
            Some(self.aggregate_to_hemispheric(&values).to_vec())
        }
    }

    /// Get a single region's value at the start of the timestep.
    pub fn at_start(&self, region: HemisphericRegion) -> FloatValue {
        let values = self.at_start_all();
        values[region as usize]
    }

    /// Get a single region's value at the end of the timestep.
    pub fn at_end(&self, region: HemisphericRegion) -> Option<FloatValue> {
        self.at_end_all().map(|v| v[region as usize])
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        self.current_time
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        self.current_index
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        self.timeseries.len()
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        self.timeseries.is_empty()
    }
}

/// A unified hemispheric window that can be either direct or aggregating.
#[derive(Debug)]
pub enum HemisphericWindow<'a> {
    /// Direct access to a Hemispheric timeseries
    Direct(GridTimeseriesWindow<'a, HemisphericGrid>),
    /// Aggregating access from a FourBox timeseries
    FromFourBox(AggregatingFourBoxToHemisphericWindow<'a>),
}

impl<'a> HemisphericWindow<'a> {
    /// Get all regional values at the start of the timestep.
    pub fn at_start_all(&self) -> Vec<FloatValue> {
        match self {
            HemisphericWindow::Direct(w) => w.at_start_all(),
            HemisphericWindow::FromFourBox(w) => w.at_start_all(),
        }
    }

    /// Get all regional values at the end of the timestep.
    pub fn at_end_all(&self) -> Option<Vec<FloatValue>> {
        match self {
            HemisphericWindow::Direct(w) => w.at_end_all(),
            HemisphericWindow::FromFourBox(w) => w.at_end_all(),
        }
    }

    /// Get a single region's value at the start of the timestep.
    pub fn at_start(&self, region: HemisphericRegion) -> FloatValue {
        match self {
            HemisphericWindow::Direct(w) => w.at_start(region),
            HemisphericWindow::FromFourBox(w) => w.at_start(region),
        }
    }

    /// Get a single region's value at the end of the timestep.
    pub fn at_end(&self, region: HemisphericRegion) -> Option<FloatValue> {
        match self {
            HemisphericWindow::Direct(w) => w.at_end(region),
            HemisphericWindow::FromFourBox(w) => w.at_end(region),
        }
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        match self {
            HemisphericWindow::Direct(w) => w.time(),
            HemisphericWindow::FromFourBox(w) => w.time(),
        }
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        match self {
            HemisphericWindow::Direct(w) => w.index(),
            HemisphericWindow::FromFourBox(w) => w.index(),
        }
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        match self {
            HemisphericWindow::Direct(w) => w.len(),
            HemisphericWindow::FromFourBox(w) => w.len(),
        }
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        match self {
            HemisphericWindow::Direct(w) => w.is_empty(),
            HemisphericWindow::FromFourBox(w) => w.is_empty(),
        }
    }

    /// Get the global aggregate at the start of the timestep.
    pub fn current_global(&self) -> FloatValue {
        let values = self.at_start_all();
        (values[0] + values[1]) / 2.0 // Simple average for now
    }
}

/// A zero-cost view into a grid timeseries at a specific time index.
///
/// `GridTimeseriesWindow` provides efficient access to regional values for spatially-resolved
/// data. It supports both individual region access and full-grid operations.
///
/// # Type Parameters
///
/// * `G` - The spatial grid type (e.g., `FourBoxGrid`, `HemisphericGrid`)
///
/// # Timestep Access Semantics
///
/// See [`TimeseriesWindow`] for detailed guidance on when to use `at_start()` vs `at_end()`.
///
/// # Examples
///
/// ```ignore
/// fn solve(&self, inputs: MyComponentInputs) -> MyComponentOutputs {
///     // Access individual regions at start of timestep
///     let northern_ocean = inputs.temperature.at_start(FourBoxRegion::NorthernOcean);
///
///     // Get all regions at once
///     let all_temps = inputs.temperature.at_start_all();
///
///     // Compute global aggregate
///     let global_temp = inputs.temperature.current_global();
/// }
/// ```
#[derive(Debug)]
pub struct GridTimeseriesWindow<'a, G>
where
    G: SpatialGrid,
{
    timeseries: &'a GridTimeseries<FloatValue, G>,
    current_index: usize,
    current_time: Time,
}

impl<'a, G> GridTimeseriesWindow<'a, G>
where
    G: SpatialGrid,
{
    /// Create a new GridTimeseriesWindow from a grid timeseries.
    pub fn new(
        timeseries: &'a GridTimeseries<FloatValue, G>,
        current_index: usize,
        current_time: Time,
    ) -> Self {
        Self {
            timeseries,
            current_index,
            current_time,
        }
    }

    /// Get all regional values at the start of the timestep (index N).
    ///
    /// Returns values for all regions in the grid at the beginning of the timestep,
    /// before any components have executed during this timestep.
    ///
    /// # When to use
    ///
    /// - **State variables**: Reading your component's previous regional state
    /// - **Exogenous inputs**: External forcing data pre-populated before the run
    ///
    /// See [`TimeseriesWindow::at_start()`] for detailed execution order semantics.
    pub fn at_start_all(&self) -> Vec<FloatValue> {
        self.timeseries
            .at_time_index(self.current_index)
            .expect("Current index out of bounds")
    }

    /// Get all regional values at the end of the timestep (index N+1), if available.
    ///
    /// Returns values for all regions written during the current timestep by upstream
    /// components that executed before you.
    ///
    /// # When to use
    ///
    /// - **Upstream component outputs**: Regional values written by components that ran before you
    /// - **Aggregation**: Combining regional outputs from multiple components in the same timestep
    ///
    /// # Returns
    ///
    /// - `Some(Vec<FloatValue>)` with values for all regions if index N+1 exists
    /// - `None` if at the last timestep
    ///
    /// See [`TimeseriesWindow::at_end()`] for detailed execution order semantics.
    pub fn at_end_all(&self) -> Option<Vec<FloatValue>> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at_time_index(next_index)
        }
    }

    /// Get all regional values at the current timestep.
    #[deprecated(
        since = "0.2.0",
        note = "Use `at_start_all()` or `at_end_all()` based on variable semantics."
    )]
    pub fn all(&self) -> Vec<FloatValue> {
        self.at_start_all()
    }

    /// Get all regional values at the previous timestep.
    pub fn previous_all(&self) -> Option<Vec<FloatValue>> {
        if self.current_index == 0 {
            None
        } else {
            self.timeseries.at_time_index(self.current_index - 1)
        }
    }

    /// Get all regional values at a relative offset from the current timestep.
    ///
    /// Positive offsets look forward in time, negative offsets look backward.
    /// Returns `None` if the resulting index is out of bounds.
    pub fn at_offset_all(&self, offset: isize) -> Option<Vec<FloatValue>> {
        let index = self.current_index as isize + offset;
        if index < 0 || index as usize >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at_time_index(index as usize)
        }
    }

    /// Get the current time value.
    pub fn time(&self) -> Time {
        self.current_time
    }

    /// Get the current time index.
    pub fn index(&self) -> usize {
        self.current_index
    }

    /// Get a reference to the underlying spatial grid.
    pub fn grid(&self) -> &G {
        self.timeseries.grid()
    }

    /// Get the total length of the underlying timeseries.
    pub fn len(&self) -> usize {
        self.timeseries.len()
    }

    /// Check if the underlying timeseries is empty.
    pub fn is_empty(&self) -> bool {
        self.timeseries.is_empty()
    }

    /// Interpolate all regional values at an arbitrary time point.
    pub fn interpolate_all(&self, t: Time) -> RSCMResult<Vec<FloatValue>> {
        self.timeseries.at_time_all(t)
    }
}

/// Type-safe accessors for FourBoxGrid windows
impl<'a> GridTimeseriesWindow<'a, FourBoxGrid> {
    /// Get a single region's value at the start of the timestep (index N).
    ///
    /// See [`TimeseriesWindow::at_start()`] for detailed semantics on when to use this method.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let inputs = MyComponentInputs::from_input_state(input_state);
    /// let northern_ocean_temp = inputs.temperature.at_start(FourBoxRegion::NorthernOcean);
    /// ```
    pub fn at_start(&self, region: FourBoxRegion) -> FloatValue {
        self.timeseries
            .at(self.current_index, region)
            .expect("Current index out of bounds")
    }

    /// Get a single region's value at the end of the timestep (index N+1), if available.
    ///
    /// See [`TimeseriesWindow::at_end()`] for detailed semantics on when to use this method.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Read upstream forcing written this timestep
    /// let erf = inputs.erf.at_end(FourBoxRegion::NorthernOcean)
    ///     .unwrap_or_else(|| inputs.erf.at_start(FourBoxRegion::NorthernOcean));
    /// ```
    pub fn at_end(&self, region: FourBoxRegion) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at(next_index, region)
        }
    }

    /// Get a single region's value at the previous timestep.
    pub fn previous(&self, region: FourBoxRegion) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            self.timeseries.at(self.current_index - 1, region)
        }
    }

    /// Get the global aggregate at the start of the timestep (index N).
    ///
    /// Uses the grid's weights to compute a weighted average of all regions.
    pub fn current_global(&self) -> FloatValue {
        let values = self.at_start_all();
        self.timeseries.grid().aggregate_global(&values)
    }

    /// Get the global aggregate at the previous timestep.
    pub fn previous_global(&self) -> Option<FloatValue> {
        self.previous_all()
            .map(|values| self.timeseries.grid().aggregate_global(&values))
    }

    /// Interpolate a single region's value at an arbitrary time.
    pub fn interpolate(&self, t: Time, region: FourBoxRegion) -> RSCMResult<FloatValue> {
        self.timeseries.at_time(t, region)
    }
}

/// Type-safe accessors for HemisphericGrid windows
impl<'a> GridTimeseriesWindow<'a, HemisphericGrid> {
    /// Get a single region's value at the start of the timestep (index N).
    ///
    /// See [`TimeseriesWindow::at_start()`] for detailed semantics on when to use this method.
    pub fn at_start(&self, region: HemisphericRegion) -> FloatValue {
        self.timeseries
            .at(self.current_index, region)
            .expect("Current index out of bounds")
    }

    /// Get a single region's value at the end of the timestep (index N+1), if available.
    ///
    /// See [`TimeseriesWindow::at_end()`] for detailed semantics on when to use this method.
    pub fn at_end(&self, region: HemisphericRegion) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at(next_index, region)
        }
    }

    /// Get a single region's value at the previous timestep.
    pub fn previous(&self, region: HemisphericRegion) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            self.timeseries.at(self.current_index - 1, region)
        }
    }

    /// Get the global aggregate at the start of the timestep (index N).
    pub fn current_global(&self) -> FloatValue {
        let values = self.at_start_all();
        self.timeseries.grid().aggregate_global(&values)
    }

    /// Get the global aggregate at the previous timestep.
    pub fn previous_global(&self) -> Option<FloatValue> {
        self.previous_all()
            .map(|values| self.timeseries.grid().aggregate_global(&values))
    }

    /// Interpolate a single region's value at an arbitrary time.
    pub fn interpolate(&self, t: Time, region: HemisphericRegion) -> RSCMResult<FloatValue> {
        self.timeseries.at_time(t, region)
    }
}

/// Type-safe accessors for ScalarGrid windows (convenience wrapper)
impl<'a> GridTimeseriesWindow<'a, ScalarGrid> {
    /// Get the scalar value at the start of the timestep (index N).
    ///
    /// See [`TimeseriesWindow::at_start()`] for detailed semantics on when to use this method.
    pub fn at_start(&self) -> FloatValue {
        self.timeseries
            .at(self.current_index, ScalarRegion::Global)
            .expect("Current index out of bounds")
    }

    /// Get the scalar value at the end of the timestep (index N+1), if available.
    ///
    /// See [`TimeseriesWindow::at_end()`] for detailed semantics on when to use this method.
    pub fn at_end(&self) -> Option<FloatValue> {
        let next_index = self.current_index + 1;
        if next_index >= self.timeseries.len() {
            None
        } else {
            self.timeseries.at(next_index, ScalarRegion::Global)
        }
    }

    /// Get the previous scalar value.
    pub fn previous(&self) -> Option<FloatValue> {
        if self.current_index == 0 {
            None
        } else {
            self.timeseries
                .at(self.current_index - 1, ScalarRegion::Global)
        }
    }

    /// Interpolate the value at an arbitrary time.
    pub fn interpolate(&self, t: Time) -> RSCMResult<FloatValue> {
        self.timeseries.at_time(t, ScalarRegion::Global)
    }
}

// =============================================================================
// Typed Output Slices
// =============================================================================

/// A zero-cost wrapper for four-box regional output values.
///
/// `FourBoxSlice` provides type-safe region access instead of raw arrays with magic indices.
/// It uses `#[repr(transparent)]` to ensure zero overhead compared to `[FloatValue; 4]`.
///
/// # Examples
///
/// ```rust
/// use rscm_core::state::FourBoxSlice;
/// use rscm_core::spatial::FourBoxRegion;
///
/// // Builder pattern for ergonomic construction
/// let slice = FourBoxSlice::new()
///     .with(FourBoxRegion::NorthernOcean, 15.0)
///     .with(FourBoxRegion::NorthernLand, 14.0)
///     .with(FourBoxRegion::SouthernOcean, 10.0)
///     .with(FourBoxRegion::SouthernLand, 9.0);
///
/// assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 15.0);
/// ```
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FourBoxSlice(pub [FloatValue; 4]);

impl FourBoxSlice {
    /// Create a new FourBoxSlice initialised with NaN values.
    ///
    /// Using NaN as the initial value ensures that any unset regions will
    /// be immediately apparent in output (as NaN propagates through calculations).
    pub fn new() -> Self {
        Self([FloatValue::NAN; 4])
    }

    /// Create a new FourBoxSlice with all regions set to the same value.
    pub fn uniform(value: FloatValue) -> Self {
        Self([value; 4])
    }

    /// Create a new FourBoxSlice from an array of values.
    ///
    /// Order: [NorthernOcean, NorthernLand, SouthernOcean, SouthernLand]
    pub fn from_array(values: [FloatValue; 4]) -> Self {
        Self(values)
    }

    /// Builder method to set a single region's value.
    ///
    /// Returns `self` for method chaining.
    pub fn with(mut self, region: FourBoxRegion, value: FloatValue) -> Self {
        self.0[region as usize] = value;
        self
    }

    /// Set a region's value (mutating).
    pub fn set(&mut self, region: FourBoxRegion, value: FloatValue) {
        self.0[region as usize] = value;
    }

    /// Get a region's value.
    pub fn get(&self, region: FourBoxRegion) -> FloatValue {
        self.0[region as usize]
    }

    /// Get a mutable reference to a region's value.
    pub fn get_mut(&mut self, region: FourBoxRegion) -> &mut FloatValue {
        &mut self.0[region as usize]
    }

    /// Get the underlying array.
    pub fn as_array(&self) -> &[FloatValue; 4] {
        &self.0
    }

    /// Get the underlying array as a mutable reference.
    pub fn as_array_mut(&mut self) -> &mut [FloatValue; 4] {
        &mut self.0
    }

    /// Convert to a Vec.
    pub fn to_vec(&self) -> Vec<FloatValue> {
        self.0.to_vec()
    }

    /// Compute the global aggregate using a grid's weights.
    pub fn aggregate_global(&self, grid: &FourBoxGrid) -> FloatValue {
        grid.aggregate_global(&self.0)
    }
}

impl Default for FourBoxSlice {
    fn default() -> Self {
        Self::new()
    }
}

impl From<[FloatValue; 4]> for FourBoxSlice {
    fn from(values: [FloatValue; 4]) -> Self {
        Self(values)
    }
}

impl From<FourBoxSlice> for [FloatValue; 4] {
    fn from(slice: FourBoxSlice) -> Self {
        slice.0
    }
}

impl From<FourBoxSlice> for Vec<FloatValue> {
    fn from(slice: FourBoxSlice) -> Self {
        slice.0.to_vec()
    }
}

impl std::ops::Index<FourBoxRegion> for FourBoxSlice {
    type Output = FloatValue;

    fn index(&self, region: FourBoxRegion) -> &Self::Output {
        &self.0[region as usize]
    }
}

impl std::ops::IndexMut<FourBoxRegion> for FourBoxSlice {
    fn index_mut(&mut self, region: FourBoxRegion) -> &mut Self::Output {
        &mut self.0[region as usize]
    }
}

/// A zero-cost wrapper for hemispheric regional output values.
///
/// Similar to `FourBoxSlice` but for the two-region hemispheric grid.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct HemisphericSlice(pub [FloatValue; 2]);

impl HemisphericSlice {
    /// Create a new HemisphericSlice initialised with NaN values.
    pub fn new() -> Self {
        Self([FloatValue::NAN; 2])
    }

    /// Create a new HemisphericSlice with both hemispheres set to the same value.
    pub fn uniform(value: FloatValue) -> Self {
        Self([value; 2])
    }

    /// Create a new HemisphericSlice from an array of values.
    ///
    /// Order: [Northern, Southern]
    pub fn from_array(values: [FloatValue; 2]) -> Self {
        Self(values)
    }

    /// Builder method to set a single hemisphere's value.
    pub fn with(mut self, region: HemisphericRegion, value: FloatValue) -> Self {
        self.0[region as usize] = value;
        self
    }

    /// Set a hemisphere's value (mutating).
    pub fn set(&mut self, region: HemisphericRegion, value: FloatValue) {
        self.0[region as usize] = value;
    }

    /// Get a hemisphere's value.
    pub fn get(&self, region: HemisphericRegion) -> FloatValue {
        self.0[region as usize]
    }

    /// Get a mutable reference to a hemisphere's value.
    pub fn get_mut(&mut self, region: HemisphericRegion) -> &mut FloatValue {
        &mut self.0[region as usize]
    }

    /// Get the underlying array.
    pub fn as_array(&self) -> &[FloatValue; 2] {
        &self.0
    }

    /// Get the underlying array as a mutable reference.
    pub fn as_array_mut(&mut self) -> &mut [FloatValue; 2] {
        &mut self.0
    }

    /// Convert to a Vec.
    pub fn to_vec(&self) -> Vec<FloatValue> {
        self.0.to_vec()
    }

    /// Compute the global aggregate using a grid's weights.
    pub fn aggregate_global(&self, grid: &HemisphericGrid) -> FloatValue {
        grid.aggregate_global(&self.0)
    }
}

impl Default for HemisphericSlice {
    fn default() -> Self {
        Self::new()
    }
}

impl From<[FloatValue; 2]> for HemisphericSlice {
    fn from(values: [FloatValue; 2]) -> Self {
        Self(values)
    }
}

impl From<HemisphericSlice> for [FloatValue; 2] {
    fn from(slice: HemisphericSlice) -> Self {
        slice.0
    }
}

impl From<HemisphericSlice> for Vec<FloatValue> {
    fn from(slice: HemisphericSlice) -> Self {
        slice.0.to_vec()
    }
}

impl std::ops::Index<HemisphericRegion> for HemisphericSlice {
    type Output = FloatValue;

    fn index(&self, region: HemisphericRegion) -> &Self::Output {
        &self.0[region as usize]
    }
}

impl std::ops::IndexMut<HemisphericRegion> for HemisphericSlice {
    fn index_mut(&mut self, region: HemisphericRegion) -> &mut Self::Output {
        &mut self.0[region as usize]
    }
}

// =============================================================================
// State Value Types
// =============================================================================

/// Represents a value that can be either scalar or spatially-resolved
///
/// `StateValue` is the enum used for both input state retrieval and output state
/// in components. It provides type-safe handling of scalar and grid-based values.
///
/// # Examples
///
/// ```rust
/// use rscm_core::state::{StateValue, FourBoxSlice, HemisphericSlice};
///
/// // Scalar value
/// let scalar = StateValue::Scalar(288.0);
/// assert_eq!(scalar.to_scalar(), 288.0);
///
/// // FourBox value
/// let four_box = StateValue::FourBox(FourBoxSlice::from_array([15.0, 14.0, 10.0, 9.0]));
/// assert_eq!(four_box.to_scalar(), 12.0); // Mean of all regions
///
/// // Hemispheric value
/// let hemispheric = StateValue::Hemispheric(HemisphericSlice::from_array([15.0, 10.0]));
/// assert_eq!(hemispheric.to_scalar(), 12.5); // Mean of both hemispheres
/// ```
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum StateValue {
    /// A single scalar value (global average or non-spatial variable)
    Scalar(FloatValue),
    /// Four-box regional values (Northern Ocean, Northern Land, Southern Ocean, Southern Land)
    FourBox(FourBoxSlice),
    /// Hemispheric values (Northern, Southern)
    Hemispheric(HemisphericSlice),
}

/// Transform context for automatic grid aggregation.
///
/// This holds the information needed to transform grid data during read operations.
/// It is passed to InputState when the model has configured grid transformations.
#[derive(Debug, Clone, Default)]
pub struct TransformContext {
    /// Map from variable name to the read transformation info
    pub read_transforms: HashMap<String, ReadTransformInfo>,
}

/// Input state for a component
///
/// A state is a collection of values
/// that can be used to represent the state of a system at a given time.
///
/// This is very similar to a Hashmap (with likely worse performance),
/// but provides strong type separation.
#[derive(Debug, Clone)]
pub struct InputState<'a> {
    current_time: Time,
    state: Vec<&'a TimeseriesItem>,
    /// Optional transform context for grid aggregation
    transform_context: Option<TransformContext>,
}

impl<'a> InputState<'a> {
    pub fn build(values: Vec<&'a TimeseriesItem>, current_time: Time) -> Self {
        Self {
            current_time,
            state: values,
            transform_context: None,
        }
    }

    /// Build an InputState with transform context for grid aggregation.
    pub fn build_with_transforms(
        values: Vec<&'a TimeseriesItem>,
        current_time: Time,
        transform_context: TransformContext,
    ) -> Self {
        Self {
            current_time,
            state: values,
            transform_context: Some(transform_context),
        }
    }

    pub fn empty() -> Self {
        Self {
            current_time: Time::nan(),
            state: vec![],
            transform_context: None,
        }
    }

    /// Get the global aggregated value for a variable
    ///
    /// For scalar variables, returns the scalar value.
    /// For grid variables, aggregates all regions to a single global value using the grid's weights.
    pub fn get_global(&self, name: &str) -> Option<FloatValue> {
        let item = self.iter().find(|item| item.name == name)?;

        match &item.data {
            TimeseriesData::Scalar(ts) => match item.variable_type {
                VariableType::Exogenous => ts.at_time(self.current_time, ScalarRegion::Global).ok(),
                VariableType::Endogenous => ts.latest_value(),
            },
            TimeseriesData::FourBox(ts) => {
                let values = match item.variable_type {
                    VariableType::Exogenous => ts.at_time_all(self.current_time).ok()?,
                    VariableType::Endogenous => ts.latest_values(),
                };
                Some(ts.grid().aggregate_global(&values))
            }
            TimeseriesData::Hemispheric(ts) => {
                let values = match item.variable_type {
                    VariableType::Exogenous => ts.at_time_all(self.current_time).ok()?,
                    VariableType::Endogenous => ts.latest_values(),
                };
                Some(ts.grid().aggregate_global(&values))
            }
        }
    }

    /// Test if the state contains a value with the given name
    pub fn has(&self, name: &str) -> bool {
        self.state.iter().any(|x| x.name == name)
    }

    pub fn iter(&self) -> impl Iterator<Item = &&TimeseriesItem> {
        self.state.iter()
    }

    /// Get the current time
    pub fn current_time(&self) -> Time {
        self.current_time
    }

    /// Get a scalar TimeseriesWindow for the named variable
    ///
    /// This provides zero-cost access to current, previous, and historical values.
    /// If the underlying data is stored at a finer grid resolution (FourBox or Hemispheric)
    /// and a read transform is configured, the data will be automatically aggregated
    /// to a scalar value on each access.
    ///
    /// # Panics
    ///
    /// Panics if the variable is not found or cannot be accessed as scalar.
    pub fn get_scalar_window(&self, name: &str) -> ScalarWindow<'_> {
        let item = self
            .iter()
            .find(|item| item.name == name)
            .unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));

        // Check if there's a read transform for this variable
        let transform = self
            .transform_context
            .as_ref()
            .and_then(|ctx| ctx.read_transforms.get(name));

        // If there's a transform, use the source grid type
        if let Some(transform) = transform {
            match transform.source_grid {
                GridType::FourBox => {
                    let ts = item.data.as_four_box().unwrap_or_else(|| {
                        panic!(
                            "Variable '{}' requires FourBox->Scalar transform but is not FourBox",
                            name
                        )
                    });

                    let current_index =
                        ts.time_axis()
                            .index_of(self.current_time)
                            .unwrap_or_else(|| {
                                panic!(
                                    "Time {} not found in timeseries '{}' time axis",
                                    self.current_time, name
                                )
                            });

                    return ScalarWindow::FromFourBox(AggregatingFourBoxWindow::new(
                        ts,
                        current_index,
                        self.current_time,
                        transform.weights.clone(),
                    ));
                }
                GridType::Hemispheric => {
                    let ts = item
                        .data
                        .as_hemispheric()
                        .unwrap_or_else(|| {
                            panic!(
                                "Variable '{}' requires Hemispheric->Scalar transform but is not Hemispheric",
                                name
                            )
                        });

                    let current_index =
                        ts.time_axis()
                            .index_of(self.current_time)
                            .unwrap_or_else(|| {
                                panic!(
                                    "Time {} not found in timeseries '{}' time axis",
                                    self.current_time, name
                                )
                            });

                    return ScalarWindow::FromHemispheric(AggregatingHemisphericWindow::new(
                        ts,
                        current_index,
                        self.current_time,
                        transform.weights.clone(),
                    ));
                }
                GridType::Scalar => {
                    // No transform needed, fall through to direct access
                }
            }
        }

        // Direct scalar access (no transform needed)
        let ts = item
            .data
            .as_scalar()
            .unwrap_or_else(|| panic!("Variable '{}' is not a scalar timeseries", name));

        let current_index = ts
            .time_axis()
            .index_of(self.current_time)
            .unwrap_or_else(|| {
                panic!(
                    "Time {} not found in timeseries '{}' time axis",
                    self.current_time, name
                )
            });

        ScalarWindow::Direct(TimeseriesWindow::new(ts, current_index, self.current_time))
    }

    /// Get a FourBox GridTimeseriesWindow for the named variable
    ///
    /// # Panics
    ///
    /// Panics if the variable is not found or is not a FourBox timeseries.
    pub fn get_four_box_window(&self, name: &str) -> GridTimeseriesWindow<'_, FourBoxGrid> {
        let item = self
            .iter()
            .find(|item| item.name == name)
            .unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));

        let ts = item
            .data
            .as_four_box()
            .unwrap_or_else(|| panic!("Variable '{}' is not a FourBox timeseries", name));

        // Find the index corresponding to current_time in the timeseries
        let current_index = ts
            .time_axis()
            .index_of(self.current_time)
            .unwrap_or_else(|| {
                panic!(
                    "Time {} not found in timeseries '{}' time axis",
                    self.current_time, name
                )
            });

        GridTimeseriesWindow::new(ts, current_index, self.current_time)
    }

    /// Get a Hemispheric GridTimeseriesWindow for the named variable
    ///
    /// # Panics
    ///
    /// If the underlying data is stored at FourBox resolution and a read transform
    /// is configured, the data will be automatically aggregated to Hemispheric
    /// on each access.
    ///
    /// # Panics
    ///
    /// Panics if the variable is not found or cannot be accessed as Hemispheric.
    pub fn get_hemispheric_window(&self, name: &str) -> HemisphericWindow<'_> {
        let item = self
            .iter()
            .find(|item| item.name == name)
            .unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));

        // Check if there's a read transform for this variable
        let transform = self
            .transform_context
            .as_ref()
            .and_then(|ctx| ctx.read_transforms.get(name));

        // If there's a transform from FourBox, aggregate
        if let Some(transform) = transform {
            if transform.source_grid == GridType::FourBox {
                let ts = item.data.as_four_box().unwrap_or_else(|| {
                    panic!(
                        "Variable '{}' requires FourBox->Hemispheric transform but is not FourBox",
                        name
                    )
                });

                let current_index =
                    ts.time_axis()
                        .index_of(self.current_time)
                        .unwrap_or_else(|| {
                            panic!(
                                "Time {} not found in timeseries '{}' time axis",
                                self.current_time, name
                            )
                        });

                return HemisphericWindow::FromFourBox(AggregatingFourBoxToHemisphericWindow::new(
                    ts,
                    current_index,
                    self.current_time,
                ));
            }
        }

        // Direct hemispheric access
        let ts = item
            .data
            .as_hemispheric()
            .unwrap_or_else(|| panic!("Variable '{}' is not a Hemispheric timeseries", name));

        let current_index = ts
            .time_axis()
            .index_of(self.current_time)
            .unwrap_or_else(|| {
                panic!(
                    "Time {} not found in timeseries '{}' time axis",
                    self.current_time, name
                )
            });

        HemisphericWindow::Direct(GridTimeseriesWindow::new(
            ts,
            current_index,
            self.current_time,
        ))
    }

    /// Converts the state into an equivalent hashmap
    ///
    /// For grid variables, aggregates to global values using grid weights.
    pub fn to_hashmap(self) -> HashMap<String, FloatValue> {
        HashMap::from_iter(self.state.into_iter().map(|item| {
            let value = match &item.data {
                TimeseriesData::Scalar(ts) => ts.latest_value().unwrap(),
                TimeseriesData::FourBox(ts) => ts.grid().aggregate_global(&ts.latest_values()),
                TimeseriesData::Hemispheric(ts) => ts.grid().aggregate_global(&ts.latest_values()),
            };
            (item.name.clone(), value)
        }))
    }
}

impl<'a> IntoIterator for InputState<'a> {
    type Item = &'a TimeseriesItem;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.state.into_iter()
    }
}

/// Output state from a component
///
/// A collection of named values that a component produces. Each value can be:
/// - `StateValue::Scalar` for global/non-spatial values
/// - `StateValue::FourBox` for four-box regional values
/// - `StateValue::Hemispheric` for hemispheric values
///
/// The model writes these values to the appropriate timeseries based on the
/// variable's grid type in `RequirementDefinition`.
pub type OutputState = HashMap<String, StateValue>;

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

    #[test]
    fn test_state_value_scalar() {
        let sv = StateValue::Scalar(42.0);
        assert!(sv.is_scalar());
        assert!(!sv.is_four_box());
        assert!(!sv.is_hemispheric());
        assert_eq!(sv.as_scalar(), Some(42.0));
        assert_eq!(sv.as_four_box(), None);
        assert_eq!(sv.as_hemispheric(), None);
        assert_eq!(sv.to_scalar(), 42.0);
    }

    #[test]
    fn test_state_value_four_box() {
        let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
        let sv = StateValue::FourBox(slice);
        assert!(!sv.is_scalar());
        assert!(sv.is_four_box());
        assert!(!sv.is_hemispheric());
        assert_eq!(sv.as_scalar(), None);
        assert_eq!(sv.as_four_box(), Some(&slice));
        assert_eq!(sv.as_hemispheric(), None);
        assert_eq!(sv.to_scalar(), 2.5); // Mean of [1, 2, 3, 4]
    }

    #[test]
    fn test_state_value_hemispheric() {
        let slice = HemisphericSlice::from_array([10.0, 20.0]);
        let sv = StateValue::Hemispheric(slice);
        assert!(!sv.is_scalar());
        assert!(!sv.is_four_box());
        assert!(sv.is_hemispheric());
        assert_eq!(sv.as_scalar(), None);
        assert_eq!(sv.as_four_box(), None);
        assert_eq!(sv.as_hemispheric(), Some(&slice));
        assert_eq!(sv.to_scalar(), 15.0); // Mean of [10, 20]
    }

    #[test]
    fn test_state_value_from_impls() {
        // Test From<FloatValue> for StateValue
        let sv: StateValue = 42.0.into();
        assert!(sv.is_scalar());
        assert_eq!(sv.as_scalar(), Some(42.0));

        // Test From<FourBoxSlice> for StateValue
        let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
        let sv: StateValue = slice.into();
        assert!(sv.is_four_box());

        // Test From<HemisphericSlice> for StateValue
        let slice = HemisphericSlice::from_array([10.0, 20.0]);
        let sv: StateValue = slice.into();
        assert!(sv.is_hemispheric());
    }

    #[test]
    fn test_input_state_get_global() {
        use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
        use crate::timeseries::{TimeAxis, Timeseries};
        use numpy::array;
        use numpy::ndarray::Axis;
        use std::sync::Arc;

        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
        let values = array![280.0, 285.0].insert_axis(Axis(1));
        let ts = Timeseries::new(
            values,
            time_axis,
            crate::spatial::ScalarGrid,
            "ppm".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );

        let item = TimeseriesItem {
            data: TimeseriesData::Scalar(ts),
            name: "CO2".to_string(),
            variable_type: VariableType::Endogenous,
        };

        // Use a time that exists in the time axis
        let state = InputState::build(vec![&item], 2001.0);

        // at_start() returns value at index corresponding to current_time (index 1)
        assert_eq!(state.get_global("CO2"), Some(285.0));
        assert_eq!(state.get_scalar_window("CO2").at_start(), 285.0);
    }

    #[test]
    fn test_input_state_grid_values() {
        use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
        use crate::spatial::FourBoxGrid;
        use crate::timeseries::{GridTimeseries, TimeAxis};
        use numpy::array;
        use numpy::ndarray::Array2;
        use std::sync::Arc;

        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
        let values =
            Array2::from_shape_vec((2, 4), vec![15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0])
                .unwrap();

        let ts = GridTimeseries::new(
            values,
            time_axis,
            grid,
            "degC".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );

        let item = TimeseriesItem {
            data: TimeseriesData::FourBox(ts),
            name: "Temperature".to_string(),
            variable_type: VariableType::Endogenous,
        };

        // Use a time that exists in the time axis
        let state = InputState::build(vec![&item], 2001.0);

        // Test get_four_box_window returns values at index 1 using at_start()
        let window = state.get_four_box_window("Temperature");
        let values = window.at_start_all();
        assert_eq!(values, [16.0, 15.0, 11.0, 10.0]);

        // Test get_global aggregates using weights (equal weights = mean)
        let global = state.get_global("Temperature").unwrap();
        assert_eq!(global, 13.0); // (16 + 15 + 11 + 10) / 4
    }

    #[test]
    fn test_input_state_to_hashmap_with_grid() {
        use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
        use crate::spatial::FourBoxGrid;
        use crate::timeseries::{GridTimeseries, TimeAxis};
        use numpy::array;
        use numpy::ndarray::Array2;
        use std::sync::Arc;

        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
        let values =
            Array2::from_shape_vec((2, 4), vec![15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0])
                .unwrap();

        let ts = GridTimeseries::new(
            values,
            time_axis,
            grid,
            "degC".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );

        let item = TimeseriesItem {
            data: TimeseriesData::FourBox(ts),
            name: "Temperature".to_string(),
            variable_type: VariableType::Endogenous,
        };

        let state = InputState::build(vec![&item], 2000.5);
        let hashmap = state.to_hashmap();

        // Should contain aggregated global value
        assert_eq!(hashmap.get("Temperature"), Some(&13.0));
    }
}

impl StateValue {
    /// Convert to a scalar value, aggregating if necessary
    ///
    /// For Scalar variants, returns the value directly.
    /// For FourBox variants, computes the mean of all 4 regional values.
    /// For Hemispheric variants, computes the mean of both hemispheres.
    ///
    /// Note: This simple averaging may not be physically appropriate for all variables.
    /// Use grid-aware aggregation methods when the grid weights are known.
    pub fn to_scalar(&self) -> FloatValue {
        match self {
            StateValue::Scalar(v) => *v,
            StateValue::FourBox(slice) => {
                let values = slice.as_array();
                values.iter().sum::<FloatValue>() / 4.0
            }
            StateValue::Hemispheric(slice) => {
                let values = slice.as_array();
                values.iter().sum::<FloatValue>() / 2.0
            }
        }
    }

    /// Check if this is a scalar value
    pub fn is_scalar(&self) -> bool {
        matches!(self, StateValue::Scalar(_))
    }

    /// Check if this is a FourBox grid value
    pub fn is_four_box(&self) -> bool {
        matches!(self, StateValue::FourBox(_))
    }

    /// Check if this is a Hemispheric grid value
    pub fn is_hemispheric(&self) -> bool {
        matches!(self, StateValue::Hemispheric(_))
    }

    /// Get the scalar value if this is a Scalar variant
    pub fn as_scalar(&self) -> Option<FloatValue> {
        match self {
            StateValue::Scalar(v) => Some(*v),
            _ => None,
        }
    }

    /// Get the FourBoxSlice if this is a FourBox variant
    pub fn as_four_box(&self) -> Option<&FourBoxSlice> {
        match self {
            StateValue::FourBox(slice) => Some(slice),
            _ => None,
        }
    }

    /// Get the HemisphericSlice if this is a Hemispheric variant
    pub fn as_hemispheric(&self) -> Option<&HemisphericSlice> {
        match self {
            StateValue::Hemispheric(slice) => Some(slice),
            _ => None,
        }
    }
}

impl From<FloatValue> for StateValue {
    fn from(value: FloatValue) -> Self {
        StateValue::Scalar(value)
    }
}

impl From<FourBoxSlice> for StateValue {
    fn from(slice: FourBoxSlice) -> Self {
        StateValue::FourBox(slice)
    }
}

impl From<HemisphericSlice> for StateValue {
    fn from(slice: HemisphericSlice) -> Self {
        StateValue::Hemispheric(slice)
    }
}

#[cfg(test)]
mod timeseries_window_tests {
    use super::*;
    use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
    use crate::timeseries::TimeAxis;
    use numpy::array;
    use numpy::ndarray::{Array, Axis};
    use std::sync::Arc;

    fn create_scalar_timeseries() -> Timeseries<FloatValue> {
        let values = array![1.0, 2.0, 3.0, 4.0, 5.0].insert_axis(Axis(1));
        let time_axis = Arc::new(TimeAxis::from_values(Array::range(2000.0, 2005.0, 1.0)));
        GridTimeseries::new(
            values,
            time_axis,
            ScalarGrid,
            "test".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        )
    }

    #[test]
    fn test_timeseries_window_at_start() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);

        // at_start() returns value at index N (the current index)
        assert_eq!(window.at_start(), 3.0);
        assert_eq!(window.time(), 2002.0);
        assert_eq!(window.index(), 2);

        // At index 0
        let window_start = TimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.at_start(), 1.0);

        // At last index
        let window_end = TimeseriesWindow::new(&ts, 4, 2004.0);
        assert_eq!(window_end.at_start(), 5.0);
    }

    #[test]
    fn test_timeseries_window_at_end() {
        let ts = create_scalar_timeseries();

        // At index 2, at_end() should return value at index 3
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);
        assert_eq!(window.at_end(), Some(4.0));

        // At index 0, at_end() should return value at index 1
        let window_start = TimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.at_end(), Some(2.0));

        // At last index, at_end() should return None (out of bounds)
        let window_end = TimeseriesWindow::new(&ts, 4, 2004.0);
        assert_eq!(window_end.at_end(), None);
    }

    #[test]
    fn test_timeseries_window_previous() {
        let ts = create_scalar_timeseries();

        // At index 2, previous should be index 1
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);
        assert_eq!(window.previous(), Some(2.0));

        // At index 0, previous should be None
        let window_start = TimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.previous(), None);
    }

    #[test]
    fn test_timeseries_window_at_offset() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);

        assert_eq!(window.at_offset(0), Some(3.0)); // Current
        assert_eq!(window.at_offset(-1), Some(2.0)); // Previous
        assert_eq!(window.at_offset(-2), Some(1.0)); // Two back
        assert_eq!(window.at_offset(1), Some(4.0)); // Next
        assert_eq!(window.at_offset(2), Some(5.0)); // Two forward
        assert_eq!(window.at_offset(-3), None); // Out of bounds
        assert_eq!(window.at_offset(3), None); // Out of bounds
    }

    #[test]
    fn test_timeseries_window_last_n() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 4, 2004.0);

        let last_3 = window.last_n(3);
        assert_eq!(last_3.len(), 3);
        assert_eq!(last_3[0], 3.0);
        assert_eq!(last_3[1], 4.0);
        assert_eq!(last_3[2], 5.0);

        let last_1 = window.last_n(1);
        assert_eq!(last_1[0], 5.0);

        let all = window.last_n(5);
        assert_eq!(all.len(), 5);
    }

    #[test]
    #[should_panic(expected = "Cannot get 6 values when only 5 available")]
    fn test_timeseries_window_last_n_panic() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 4, 2004.0);
        let _ = window.last_n(6); // Only 5 values available
    }

    #[test]
    fn test_timeseries_window_interpolate() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);

        let mid = window.interpolate(2001.5).unwrap();
        assert_eq!(mid, 2.5); // Linear interpolation between 2.0 and 3.0
    }

    #[test]
    fn test_timeseries_window_len() {
        let ts = create_scalar_timeseries();
        let window = TimeseriesWindow::new(&ts, 2, 2002.0);

        assert_eq!(window.len(), 5);
        assert!(!window.is_empty());
    }
}

#[cfg(test)]
mod grid_timeseries_window_tests {
    use super::*;
    use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
    use crate::spatial::FourBoxGrid;
    use crate::timeseries::TimeAxis;
    use numpy::array;
    use numpy::ndarray::Array2;
    use std::sync::Arc;

    fn create_four_box_timeseries() -> GridTimeseries<FloatValue, FourBoxGrid> {
        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
        let values = Array2::from_shape_vec(
            (3, 4),
            vec![
                15.0, 14.0, 10.0, 9.0, // 2000
                16.0, 15.0, 11.0, 10.0, // 2001
                17.0, 16.0, 12.0, 11.0, // 2002
            ],
        )
        .unwrap();

        GridTimeseries::new(
            values,
            time_axis,
            grid,
            "C".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        )
    }

    #[test]
    fn test_grid_window_at_start() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        // at_start() returns value at index N (the current index)
        assert_eq!(window.at_start(FourBoxRegion::NorthernOcean), 16.0);
        assert_eq!(window.at_start(FourBoxRegion::NorthernLand), 15.0);
        assert_eq!(window.at_start(FourBoxRegion::SouthernOcean), 11.0);
        assert_eq!(window.at_start(FourBoxRegion::SouthernLand), 10.0);

        // At first index
        let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.at_start(FourBoxRegion::NorthernOcean), 15.0);

        // At last index
        let window_end = GridTimeseriesWindow::new(&ts, 2, 2002.0);
        assert_eq!(window_end.at_start(FourBoxRegion::NorthernOcean), 17.0);
    }

    #[test]
    fn test_grid_window_at_end() {
        let ts = create_four_box_timeseries();

        // At index 1, at_end() should return value at index 2
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
        assert_eq!(
            window.at_end(FourBoxRegion::NorthernOcean),
            Some(17.0) // 2002 value
        );

        // At index 0, at_end() should return value at index 1
        let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(
            window_start.at_end(FourBoxRegion::NorthernOcean),
            Some(16.0)
        );

        // At last index, at_end() should return None (out of bounds)
        let window_end = GridTimeseriesWindow::new(&ts, 2, 2002.0);
        assert_eq!(window_end.at_end(FourBoxRegion::NorthernOcean), None);
    }

    #[test]
    fn test_grid_window_at_start_all() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        let all = window.at_start_all();
        assert_eq!(all, vec![16.0, 15.0, 11.0, 10.0]);
    }

    #[test]
    fn test_grid_window_at_end_all() {
        let ts = create_four_box_timeseries();

        // At index 1, at_end returns values at index 2
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
        assert_eq!(window.at_end_all(), Some(vec![17.0, 16.0, 12.0, 11.0]));

        // At last index, returns None
        let window_end = GridTimeseriesWindow::new(&ts, 2, 2002.0);
        assert_eq!(window_end.at_end_all(), None);
    }

    #[test]
    #[allow(deprecated)]
    fn test_grid_window_all() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        // all() is deprecated alias for at_start_all()
        let all = window.all();
        assert_eq!(all, vec![16.0, 15.0, 11.0, 10.0]);
        assert_eq!(all, window.at_start_all());
    }

    #[test]
    fn test_grid_window_previous() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        assert_eq!(window.previous(FourBoxRegion::NorthernOcean), Some(15.0));
        assert_eq!(window.previous_all(), Some(vec![15.0, 14.0, 10.0, 9.0]));

        let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.previous(FourBoxRegion::NorthernOcean), None);
        assert_eq!(window_start.previous_all(), None);
    }

    #[test]
    fn test_grid_window_current_global() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        // Equal weights: (16 + 15 + 11 + 10) / 4 = 13.0
        assert_eq!(window.current_global(), 13.0);
    }

    #[test]
    fn test_grid_window_previous_global() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        // (15 + 14 + 10 + 9) / 4 = 12.0
        assert_eq!(window.previous_global(), Some(12.0));

        let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
        assert_eq!(window_start.previous_global(), None);
    }

    #[test]
    fn test_grid_window_interpolate() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        // Interpolate at midpoint between 2000 and 2001
        let mid = window
            .interpolate(2000.5, FourBoxRegion::NorthernOcean)
            .unwrap();
        assert_eq!(mid, 15.5); // Linear interpolation between 15.0 and 16.0
    }

    #[test]
    fn test_grid_window_interpolate_all() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        let mid = window.interpolate_all(2000.5).unwrap();
        assert_eq!(mid, vec![15.5, 14.5, 10.5, 9.5]);
    }

    #[test]
    fn test_grid_window_metadata() {
        let ts = create_four_box_timeseries();
        let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);

        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
        assert_eq!(window.grid().size(), 4);
    }
}

#[cfg(test)]
mod typed_slice_tests {
    use super::*;
    use crate::spatial::{FourBoxGrid, HemisphericGrid};

    #[test]
    fn test_four_box_slice_new() {
        let slice = FourBoxSlice::new();
        assert!(slice.get(FourBoxRegion::NorthernOcean).is_nan());
        assert!(slice.get(FourBoxRegion::SouthernLand).is_nan());
    }

    #[test]
    fn test_four_box_slice_uniform() {
        let slice = FourBoxSlice::uniform(15.0);
        assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 15.0);
        assert_eq!(slice.get(FourBoxRegion::SouthernLand), 15.0);
    }

    #[test]
    fn test_four_box_slice_builder() {
        let slice = FourBoxSlice::new()
            .with(FourBoxRegion::NorthernOcean, 16.0)
            .with(FourBoxRegion::NorthernLand, 15.0)
            .with(FourBoxRegion::SouthernOcean, 11.0)
            .with(FourBoxRegion::SouthernLand, 10.0);

        assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 16.0);
        assert_eq!(slice.get(FourBoxRegion::NorthernLand), 15.0);
        assert_eq!(slice.get(FourBoxRegion::SouthernOcean), 11.0);
        assert_eq!(slice.get(FourBoxRegion::SouthernLand), 10.0);
    }

    #[test]
    fn test_four_box_slice_mutate() {
        let mut slice = FourBoxSlice::uniform(0.0);
        slice.set(FourBoxRegion::NorthernOcean, 42.0);
        assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 42.0);

        *slice.get_mut(FourBoxRegion::SouthernLand) = 7.0;
        assert_eq!(slice.get(FourBoxRegion::SouthernLand), 7.0);
    }

    #[test]
    fn test_four_box_slice_index() {
        let mut slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
        assert_eq!(slice[FourBoxRegion::NorthernOcean], 1.0);
        assert_eq!(slice[FourBoxRegion::NorthernLand], 2.0);

        slice[FourBoxRegion::SouthernOcean] = 99.0;
        assert_eq!(slice[FourBoxRegion::SouthernOcean], 99.0);
    }

    #[test]
    fn test_four_box_slice_conversions() {
        let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);

        let vec: Vec<FloatValue> = slice.into();
        assert_eq!(vec, vec![1.0, 2.0, 3.0, 4.0]);

        let slice2: FourBoxSlice = [5.0, 6.0, 7.0, 8.0].into();
        let arr: [FloatValue; 4] = slice2.into();
        assert_eq!(arr, [5.0, 6.0, 7.0, 8.0]);
    }

    #[test]
    fn test_four_box_slice_aggregate_global() {
        let slice = FourBoxSlice::from_array([16.0, 14.0, 12.0, 10.0]);
        let grid = FourBoxGrid::magicc_standard();
        let global = slice.aggregate_global(&grid);
        // Equal weights: (16 + 14 + 12 + 10) / 4 = 13.0
        assert_eq!(global, 13.0);
    }

    #[test]
    fn test_hemispheric_slice_new() {
        let slice = HemisphericSlice::new();
        assert!(slice.get(HemisphericRegion::Northern).is_nan());
        assert!(slice.get(HemisphericRegion::Southern).is_nan());
    }

    #[test]
    fn test_hemispheric_slice_builder() {
        let slice = HemisphericSlice::new()
            .with(HemisphericRegion::Northern, 15.0)
            .with(HemisphericRegion::Southern, 10.0);

        assert_eq!(slice.get(HemisphericRegion::Northern), 15.0);
        assert_eq!(slice.get(HemisphericRegion::Southern), 10.0);
    }

    #[test]
    fn test_hemispheric_slice_index() {
        let mut slice = HemisphericSlice::from_array([15.0, 10.0]);
        assert_eq!(slice[HemisphericRegion::Northern], 15.0);
        assert_eq!(slice[HemisphericRegion::Southern], 10.0);

        slice[HemisphericRegion::Northern] = 20.0;
        assert_eq!(slice[HemisphericRegion::Northern], 20.0);
    }

    #[test]
    fn test_hemispheric_slice_aggregate_global() {
        let slice = HemisphericSlice::from_array([15.0, 10.0]);
        let grid = HemisphericGrid::equal_weights();
        let global = slice.aggregate_global(&grid);
        // Equal weights: (15 + 10) / 2 = 12.5
        assert_eq!(global, 12.5);
    }

    #[test]
    fn test_slice_default() {
        let four_box = FourBoxSlice::default();
        assert!(four_box.get(FourBoxRegion::NorthernOcean).is_nan());

        let hemispheric = HemisphericSlice::default();
        assert!(hemispheric.get(HemisphericRegion::Northern).is_nan());
    }
}

#[cfg(test)]
mod input_state_window_tests {
    use super::*;
    use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
    use crate::spatial::{FourBoxGrid, HemisphericGrid};
    use crate::timeseries::{GridTimeseries, TimeAxis, Timeseries};
    use numpy::array;
    use numpy::ndarray::{Array2, Axis};
    use std::sync::Arc;

    fn create_scalar_item(name: &str, values: Vec<FloatValue>) -> TimeseriesItem {
        // Create time axis that matches values length
        let n = values.len();
        let time_vals: Vec<f64> = (0..n).map(|i| 2000.0 + i as f64).collect();
        let time_axis = Arc::new(TimeAxis::from_values(ndarray::Array1::from_vec(time_vals)));
        let values_arr = ndarray::Array1::from_vec(values).insert_axis(Axis(1));
        let ts = Timeseries::new(
            values_arr,
            time_axis,
            ScalarGrid,
            "unit".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );
        TimeseriesItem {
            data: TimeseriesData::Scalar(ts),
            name: name.to_string(),
            variable_type: VariableType::Endogenous,
        }
    }

    fn create_four_box_item(name: &str) -> TimeseriesItem {
        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
        let values = Array2::from_shape_vec(
            (3, 4),
            vec![
                15.0, 14.0, 10.0, 9.0, // 2000
                16.0, 15.0, 11.0, 10.0, // 2001
                17.0, 16.0, 12.0, 11.0, // 2002
            ],
        )
        .unwrap();
        let ts = GridTimeseries::new(
            values,
            time_axis,
            grid,
            "C".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );
        TimeseriesItem {
            data: TimeseriesData::FourBox(ts),
            name: name.to_string(),
            variable_type: VariableType::Endogenous,
        }
    }

    fn create_hemispheric_item(name: &str) -> TimeseriesItem {
        let grid = HemisphericGrid::equal_weights();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
        let values = Array2::from_shape_vec(
            (2, 2),
            vec![
                1000.0, 500.0, // 2000
                1100.0, 550.0, // 2001
            ],
        )
        .unwrap();
        let ts = GridTimeseries::new(
            values,
            time_axis,
            grid,
            "mm/yr".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );
        TimeseriesItem {
            data: TimeseriesData::Hemispheric(ts),
            name: name.to_string(),
            variable_type: VariableType::Endogenous,
        }
    }

    #[test]
    fn test_get_scalar_window() {
        let item = create_scalar_item("CO2", vec![280.0, 285.0, 290.0, 295.0, 300.0]);
        // Time 2002.0 corresponds to index 2 in the timeseries [2000, 2001, 2002, 2003, 2004]
        let state = InputState::build(vec![&item], 2002.0);

        let window = state.get_scalar_window("CO2");

        // at_start() returns the value at the index corresponding to current_time
        assert_eq!(window.at_start(), 290.0);
        assert_eq!(window.at_end().unwrap(), 295.0);
        assert_eq!(window.previous(), Some(285.0));
        assert_eq!(window.len(), 5);
    }

    #[test]
    fn test_get_four_box_window() {
        let item = create_four_box_item("Temperature");
        // Time 2001.0 corresponds to index 1 in the timeseries [2000, 2001, 2002]
        let state = InputState::build(vec![&item], 2001.0);

        let window = state.get_four_box_window("Temperature");

        // at_start() returns values at index 1 (2001 values: [16.0, 15.0, 11.0, 10.0])
        assert_eq!(window.at_start(FourBoxRegion::NorthernOcean), 16.0);
        assert_eq!(window.at_start(FourBoxRegion::SouthernLand), 10.0);
        assert_eq!(window.at_start_all(), vec![16.0, 15.0, 11.0, 10.0]);
        // previous is index 0 (2000 values: [15.0, 14.0, 10.0, 9.0])
        assert_eq!(window.previous_all(), Some(vec![15.0, 14.0, 10.0, 9.0]));
    }

    #[test]
    fn test_get_hemispheric_window() {
        let item = create_hemispheric_item("Precipitation");
        // Time 2001.0 corresponds to index 1 in the timeseries [2000, 2001]
        let state = InputState::build(vec![&item], 2001.0);

        let window = state.get_hemispheric_window("Precipitation");

        // at_start() returns values at index 1 (2001 values: [1100.0, 550.0])
        assert_eq!(window.at_start(HemisphericRegion::Northern), 1100.0);
        assert_eq!(window.at_start(HemisphericRegion::Southern), 550.0);
        assert_eq!(window.current_global(), 825.0); // Equal weights mean
    }

    #[test]
    #[should_panic(expected = "Variable 'NonExistent' not found")]
    fn test_get_scalar_window_missing_variable() {
        let item = create_scalar_item("CO2", vec![280.0, 285.0]);
        let state = InputState::build(vec![&item], 2000.0);
        let _ = state.get_scalar_window("NonExistent");
    }

    #[test]
    #[should_panic(expected = "not a scalar timeseries")]
    fn test_get_scalar_window_wrong_type() {
        let item = create_four_box_item("Temperature");
        let state = InputState::build(vec![&item], 2000.0);
        // Attempting to get scalar window for a FourBox variable should panic
        let _ = state.get_scalar_window("Temperature");
    }

    #[test]
    #[should_panic(expected = "not a FourBox timeseries")]
    fn test_get_four_box_window_wrong_type() {
        let item = create_scalar_item("CO2", vec![280.0, 285.0]);
        let state = InputState::build(vec![&item], 2000.0);
        let _ = state.get_four_box_window("CO2");
    }

    #[test]
    fn test_current_time_accessor() {
        let item = create_scalar_item("CO2", vec![280.0, 285.0]);
        let state = InputState::build(vec![&item], 2023.5);
        assert_eq!(state.current_time(), 2023.5);
    }
}

#[cfg(test)]
mod aggregating_window_tests {
    use super::*;
    use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
    use crate::spatial::{FourBoxGrid, HemisphericGrid};
    use crate::timeseries::{GridTimeseries, TimeAxis};
    use numpy::array;
    use numpy::ndarray::Array2;
    use std::sync::Arc;

    fn create_four_box_timeseries() -> GridTimeseries<FloatValue, FourBoxGrid> {
        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
        // Values chosen for easy arithmetic: each timestep increases by 1
        let values = Array2::from_shape_vec(
            (3, 4),
            vec![
                10.0, 20.0, 30.0, 40.0, // 2000: mean = 25.0
                11.0, 21.0, 31.0, 41.0, // 2001: mean = 26.0
                12.0, 22.0, 32.0, 42.0, // 2002: mean = 27.0
            ],
        )
        .unwrap();

        GridTimeseries::new(
            values,
            time_axis,
            grid,
            "W/m^2".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        )
    }

    fn create_hemispheric_timeseries() -> GridTimeseries<FloatValue, HemisphericGrid> {
        let grid = HemisphericGrid::equal_weights();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
        let values = Array2::from_shape_vec(
            (3, 2),
            vec![
                100.0, 200.0, // 2000: mean = 150.0
                110.0, 220.0, // 2001: mean = 165.0
                120.0, 240.0, // 2002: mean = 180.0
            ],
        )
        .unwrap();

        GridTimeseries::new(
            values,
            time_axis,
            grid,
            "W/m^2".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        )
    }

    // =========================================================================
    // AggregatingFourBoxWindow tests (FourBox -> Scalar aggregation)
    // =========================================================================

    #[test]
    fn test_aggregating_four_box_window_at_start_default_weights() {
        let ts = create_four_box_timeseries();
        // Index 1 = year 2001, values [11, 21, 31, 41]
        let window = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, None);

        // With equal weights: (11 + 21 + 31 + 41) / 4 = 26.0
        assert_eq!(window.at_start(), 26.0);
    }

    #[test]
    fn test_aggregating_four_box_window_at_start_custom_weights() {
        let ts = create_four_box_timeseries();
        // Custom weights that sum to 1.0
        let weights = vec![0.5, 0.2, 0.2, 0.1];
        let window = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, Some(weights));

        // With custom weights: 11*0.5 + 21*0.2 + 31*0.2 + 41*0.1 = 5.5 + 4.2 + 6.2 + 4.1 = 20.0
        assert_eq!(window.at_start(), 20.0);
    }

    #[test]
    fn test_aggregating_four_box_window_at_end() {
        let ts = create_four_box_timeseries();
        // Index 1, at_end should return value at index 2
        let window = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, None);

        // Index 2 values [12, 22, 32, 42], mean = 27.0
        assert_eq!(window.at_end(), Some(27.0));
    }

    #[test]
    fn test_aggregating_four_box_window_at_end_last_index() {
        let ts = create_four_box_timeseries();
        // At last index, at_end should return None
        let window = AggregatingFourBoxWindow::new(&ts, 2, 2002.0, None);

        assert_eq!(window.at_end(), None);
    }

    #[test]
    fn test_aggregating_four_box_window_previous() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, None);

        // Index 0 values [10, 20, 30, 40], mean = 25.0
        assert_eq!(window.previous(), Some(25.0));
    }

    #[test]
    fn test_aggregating_four_box_window_previous_at_first() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxWindow::new(&ts, 0, 2000.0, None);

        assert_eq!(window.previous(), None);
    }

    #[test]
    fn test_aggregating_four_box_window_metadata() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, None);

        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
    }

    #[test]
    fn test_aggregating_four_box_window_nan_handling() {
        // Test that NaN values are excluded from aggregation
        let grid = FourBoxGrid::magicc_standard();
        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
        let values = Array2::from_shape_vec(
            (2, 4),
            vec![
                10.0,
                f64::NAN,
                30.0,
                40.0, // 2000: has NaN
                20.0,
                20.0,
                20.0,
                20.0, // 2001: all valid
            ],
        )
        .unwrap();
        let ts = GridTimeseries::new(
            values,
            time_axis,
            grid,
            "test".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );

        let weights = vec![0.25, 0.25, 0.25, 0.25];
        let window = AggregatingFourBoxWindow::new(&ts, 0, 2000.0, Some(weights));

        // NaN is skipped: 10*0.25 + 30*0.25 + 40*0.25 = 2.5 + 7.5 + 10.0 = 20.0
        assert_eq!(window.at_start(), 20.0);
    }

    // =========================================================================
    // AggregatingHemisphericWindow tests (Hemispheric -> Scalar aggregation)
    // =========================================================================

    #[test]
    fn test_aggregating_hemispheric_window_at_start_default_weights() {
        let ts = create_hemispheric_timeseries();
        let window = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, None);

        // Equal weights: (110 + 220) / 2 = 165.0
        assert_eq!(window.at_start(), 165.0);
    }

    #[test]
    fn test_aggregating_hemispheric_window_at_start_custom_weights() {
        let ts = create_hemispheric_timeseries();
        let weights = vec![0.7, 0.3];
        let window = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, Some(weights));

        // 110*0.7 + 220*0.3 = 77.0 + 66.0 = 143.0
        assert_eq!(window.at_start(), 143.0);
    }

    #[test]
    fn test_aggregating_hemispheric_window_at_end() {
        let ts = create_hemispheric_timeseries();
        let window = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, None);

        // Index 2 values [120, 240], mean = 180.0
        assert_eq!(window.at_end(), Some(180.0));
    }

    #[test]
    fn test_aggregating_hemispheric_window_at_end_last_index() {
        let ts = create_hemispheric_timeseries();
        let window = AggregatingHemisphericWindow::new(&ts, 2, 2002.0, None);

        assert_eq!(window.at_end(), None);
    }

    #[test]
    fn test_aggregating_hemispheric_window_previous() {
        let ts = create_hemispheric_timeseries();
        let window = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, None);

        // Index 0 values [100, 200], mean = 150.0
        assert_eq!(window.previous(), Some(150.0));
    }

    #[test]
    fn test_aggregating_hemispheric_window_metadata() {
        let ts = create_hemispheric_timeseries();
        let window = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, None);

        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
    }

    // =========================================================================
    // AggregatingFourBoxToHemisphericWindow tests (FourBox -> Hemispheric)
    // =========================================================================

    #[test]
    fn test_aggregating_four_box_to_hemispheric_at_start_all() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxToHemisphericWindow::new(&ts, 1, 2001.0);

        // Index 1 values [11, 21, 31, 41]
        // Northern = (11 + 21) / 2 = 16.0
        // Southern = (31 + 41) / 2 = 36.0
        let result = window.at_start_all();
        assert_eq!(result.len(), 2);
        assert_eq!(result[0], 16.0); // Northern
        assert_eq!(result[1], 36.0); // Southern
    }

    #[test]
    fn test_aggregating_four_box_to_hemispheric_at_start_single_region() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxToHemisphericWindow::new(&ts, 1, 2001.0);

        assert_eq!(window.at_start(HemisphericRegion::Northern), 16.0);
        assert_eq!(window.at_start(HemisphericRegion::Southern), 36.0);
    }

    #[test]
    fn test_aggregating_four_box_to_hemispheric_at_end_all() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxToHemisphericWindow::new(&ts, 1, 2001.0);

        // Index 2 values [12, 22, 32, 42]
        // Northern = (12 + 22) / 2 = 17.0
        // Southern = (32 + 42) / 2 = 37.0
        let result = window.at_end_all().unwrap();
        assert_eq!(result.len(), 2);
        assert_eq!(result[0], 17.0);
        assert_eq!(result[1], 37.0);
    }

    #[test]
    fn test_aggregating_four_box_to_hemispheric_at_end_last_index() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxToHemisphericWindow::new(&ts, 2, 2002.0);

        assert_eq!(window.at_end_all(), None);
        assert_eq!(window.at_end(HemisphericRegion::Northern), None);
    }

    #[test]
    fn test_aggregating_four_box_to_hemispheric_metadata() {
        let ts = create_four_box_timeseries();
        let window = AggregatingFourBoxToHemisphericWindow::new(&ts, 1, 2001.0);

        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
    }

    // =========================================================================
    // ScalarWindow enum tests (unified scalar interface)
    // =========================================================================

    #[test]
    fn test_scalar_window_direct_variant() {
        use crate::timeseries::Timeseries;
        use numpy::ndarray::Axis;

        let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
        let values = array![100.0, 200.0, 300.0].insert_axis(Axis(1));
        let ts = Timeseries::new(
            values,
            time_axis,
            ScalarGrid,
            "test".to_string(),
            InterpolationStrategy::from(LinearSplineStrategy::new(true)),
        );
        let inner = TimeseriesWindow::new(&ts, 1, 2001.0);
        let window = ScalarWindow::Direct(inner);

        assert_eq!(window.at_start(), 200.0);
        assert_eq!(window.at_end(), Some(300.0));
        assert_eq!(window.previous(), Some(100.0));
        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
    }

    #[test]
    fn test_scalar_window_from_four_box_variant() {
        let ts = create_four_box_timeseries();
        let inner = AggregatingFourBoxWindow::new(&ts, 1, 2001.0, None);
        let window = ScalarWindow::FromFourBox(inner);

        // Same behavior as AggregatingFourBoxWindow
        assert_eq!(window.at_start(), 26.0);
        assert_eq!(window.at_end(), Some(27.0));
        assert_eq!(window.previous(), Some(25.0));
        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
    }

    #[test]
    fn test_scalar_window_from_hemispheric_variant() {
        let ts = create_hemispheric_timeseries();
        let inner = AggregatingHemisphericWindow::new(&ts, 1, 2001.0, None);
        let window = ScalarWindow::FromHemispheric(inner);

        // Same behavior as AggregatingHemisphericWindow
        assert_eq!(window.at_start(), 165.0);
        assert_eq!(window.at_end(), Some(180.0));
        assert_eq!(window.previous(), Some(150.0));
        assert_eq!(window.time(), 2001.0);
    }

    // =========================================================================
    // HemisphericWindow enum tests (unified hemispheric interface)
    // =========================================================================

    #[test]
    fn test_hemispheric_window_direct_variant() {
        let ts = create_hemispheric_timeseries();
        let inner = GridTimeseriesWindow::new(&ts, 1, 2001.0);
        let window = HemisphericWindow::Direct(inner);

        assert_eq!(window.at_start(HemisphericRegion::Northern), 110.0);
        assert_eq!(window.at_start(HemisphericRegion::Southern), 220.0);
        assert_eq!(window.at_start_all(), vec![110.0, 220.0]);
        assert_eq!(window.at_end_all(), Some(vec![120.0, 240.0]));
        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
        assert_eq!(window.len(), 3);
        assert!(!window.is_empty());
    }

    #[test]
    fn test_hemispheric_window_from_four_box_variant() {
        let ts = create_four_box_timeseries();
        let inner = AggregatingFourBoxToHemisphericWindow::new(&ts, 1, 2001.0);
        let window = HemisphericWindow::FromFourBox(inner);

        // Aggregated values: Northern=(11+21)/2=16, Southern=(31+41)/2=36
        assert_eq!(window.at_start(HemisphericRegion::Northern), 16.0);
        assert_eq!(window.at_start(HemisphericRegion::Southern), 36.0);
        assert_eq!(window.at_start_all(), vec![16.0, 36.0]);
        // Index 2: Northern=(12+22)/2=17, Southern=(32+42)/2=37
        assert_eq!(window.at_end_all(), Some(vec![17.0, 37.0]));
        assert_eq!(window.time(), 2001.0);
        assert_eq!(window.index(), 1);
    }
}
