//! TSM (Time Series Merge) module for Skulk.
//!
//! This module provides the core components for time series data storage:
//!
//! - [`TimePartition`]: Time-based partition identifier
//! - [`SeriesId`]: Unique identifier for a time series
//! - [`DataPoint`]: A single time series data point
//! - [`TimeSeriesMemTable`]: In-memory buffer for data points
//! - [`PartitionManager`]: Multi-partition routing and management
//! - [`CompressedBlock`]: Gorilla-compressed data block
//! - [`TsmWriter`] / [`TsmReader`]: File I/O for TSM format

pub mod file;
pub mod gorilla;
pub mod memtable;
pub mod partition;

use serde::{Deserialize, Serialize};
use std::time::Duration;

/// Unique identifier for a time series.
///
/// Generated by hashing the metric name and sorted labels.
pub type SeriesId = u64;

/// Timestamp in nanoseconds since Unix epoch.
pub type Timestamp = i64;

/// Time-based partition identifier.
///
/// Each partition covers a fixed duration starting from `start_ts`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TimePartition {
    /// Start timestamp of the partition (nanoseconds since Unix epoch).
    pub start_ts: i64,
    /// Duration of the partition.
    pub duration: Duration,
}

impl TimePartition {
    /// Creates a new time partition.
    ///
    /// # Arguments
    ///
    /// * `start_ts` - Start timestamp in nanoseconds
    /// * `duration` - Duration of the partition
    pub fn new(start_ts: i64, duration: Duration) -> Self {
        Self { start_ts, duration }
    }

    /// Returns the end timestamp of this partition (exclusive).
    pub fn end_ts(&self) -> i64 {
        self.start_ts + self.duration.as_nanos() as i64
    }

    /// Checks if a timestamp falls within this partition.
    pub fn contains(&self, ts: Timestamp) -> bool {
        ts >= self.start_ts && ts < self.end_ts()
    }
}

/// Metadata for a time series.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeriesMeta {
    /// Name of the metric (e.g., "cpu.usage").
    pub metric_name: String,
    /// Key-value labels for the series (e.g., [("host", "server1")]).
    pub labels: Vec<(String, String)>,
}

impl SeriesMeta {
    /// Creates new series metadata.
    pub fn new(metric_name: impl Into<String>, labels: Vec<(String, String)>) -> Self {
        Self {
            metric_name: metric_name.into(),
            labels,
        }
    }
}

/// A single time series data point.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataPoint {
    /// Name of the metric.
    pub metric: String,
    /// Key-value labels for the series.
    pub labels: Vec<(String, String)>,
    /// Timestamp in nanoseconds since Unix epoch.
    pub timestamp: i64,
    /// The data point value.
    pub value: f64,
}

impl DataPoint {
    /// Creates a new data point.
    pub fn new(
        metric: impl Into<String>,
        labels: Vec<(String, String)>,
        timestamp: i64,
        value: f64,
    ) -> Self {
        Self {
            metric: metric.into(),
            labels,
            timestamp,
            value,
        }
    }
}

/// Time range for queries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeRange {
    /// Start timestamp (inclusive).
    pub start: Timestamp,
    /// End timestamp (exclusive).
    pub end: Timestamp,
}

impl TimeRange {
    /// Creates a new time range.
    pub fn new(start: Timestamp, end: Timestamp) -> Self {
        Self { start, end }
    }

    /// Checks if a timestamp falls within this range.
    pub fn contains(&self, ts: Timestamp) -> bool {
        ts >= self.start && ts < self.end
    }
}

// Re-export main types from submodules
pub use file::{
    BloomFilter, CompressionType, SectionFlags, SeriesIndex, SeriesIndexEntry, TimestampEncoding,
    TsmDataBlock, TsmDataBlockV2, TsmFileHandle, TsmFooter, TsmHeader, TsmReader, TsmScanIterator,
    TsmWriter, ValueEncoding, FOOTER_SIZE, HEADER_SIZE, TSM_MAGIC, TSM_MAGIC_REVERSE, TSM_VERSION,
};
pub use gorilla::CompressedBlock;
pub use memtable::TimeSeriesMemTable;
pub use partition::{
    FlushPriority, PartitionManager, PartitionManagerConfig, PartitionManagerStats,
    DEFAULT_MAX_ACTIVE_PARTITIONS, DEFAULT_PARTITION_AGE_THRESHOLD, DEFAULT_PARTITION_DURATION,
    DEFAULT_PARTITION_SIZE_THRESHOLD,
};

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

    #[test]
    fn test_time_partition_contains() {
        // start_ts = 1000, duration = 1 second = 1_000_000_000 nanos
        // end_ts = 1000 + 1_000_000_000 = 1_000_001_000
        let partition = TimePartition::new(1000, Duration::from_secs(1));

        assert!(!partition.contains(999)); // before start
        assert!(partition.contains(1000)); // at start
        assert!(partition.contains(500_000_000)); // middle
        assert!(partition.contains(1_000_000_999)); // just before end
        assert!(!partition.contains(1_000_001_000)); // at end (exclusive)
    }

    #[test]
    fn test_time_partition_end_ts() {
        let partition = TimePartition::new(0, Duration::from_secs(3600));
        assert_eq!(partition.end_ts(), 3_600_000_000_000); // 1 hour in nanos
    }

    #[test]
    fn test_time_range_contains() {
        let range = TimeRange::new(100, 200);

        assert!(!range.contains(99));
        assert!(range.contains(100));
        assert!(range.contains(150));
        assert!(range.contains(199));
        assert!(!range.contains(200));
    }

    #[test]
    fn test_series_meta_new() {
        let meta = SeriesMeta::new(
            "cpu.usage",
            vec![("host".to_string(), "server1".to_string())],
        );
        assert_eq!(meta.metric_name, "cpu.usage");
        assert_eq!(meta.labels.len(), 1);
    }

    #[test]
    fn test_data_point_new() {
        let point = DataPoint::new(
            "cpu.usage",
            vec![("host".to_string(), "server1".to_string())],
            1234567890,
            0.75,
        );
        assert_eq!(point.metric, "cpu.usage");
        assert_eq!(point.timestamp, 1234567890);
        assert!((point.value - 0.75).abs() < f64::EPSILON);
    }
}
