//! Gateway fetch domain types: time granularity, aggregation, post-processing config.

use chrono::{DateTime, Utc};
use serde_json::Value;
use std::str::FromStr;

use crate::utils::format::normalize_column_name;

/// Granularity options used for grouping rows by timestamp.
#[derive(Debug)]
#[doc(hidden)]
pub enum TimeGranularity {
    /// Group rows on a per-day basis.
    Day,
    /// Group rows on a per-hour basis.
    Hour,
    /// Group rows on a per-minute basis.
    Minute,
}

impl FromStr for TimeGranularity {
    type Err = String;

    /// Parses a time granularity from a string.
    ///
    /// # Errors
    ///
    /// Returns an error if the string is not a valid time granularity.
    ///
    /// # Examples
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("day");
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("hour");
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("minute");
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("invalid");
    /// ```
    ///
    /// # Returns
    ///
    /// Returns the time granularity if the string is a valid time granularity.
    ///
    /// # Examples
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("day");
    /// assert_eq!(granularity, TimeGranularity::Day);
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("hour");
    /// assert_eq!(granularity, TimeGranularity::Hour);
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("minute");
    /// assert_eq!(granularity, TimeGranularity::Minute);
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("invalid");
    /// assert_eq!(granularity, Err(String::from("Unsupported time_granularity 'invalid'")));
    /// ```
    ///
    /// # Returns
    ///
    /// Returns the time granularity if the string is a valid time granularity.
    ///
    /// # Examples
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("day");
    /// assert_eq!(granularity, TimeGranularity::Day);
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("hour");
    /// assert_eq!(granularity, TimeGranularity::Hour);
    /// ```
    ///
    /// ```
    /// let granularity = TimeGranularity::from_str("minute");
    /// assert_eq!(granularity, TimeGranularity::Minute);
    /// ```
    ///
    fn from_str(value: &str) -> Result<Self, String> {
        match value.to_lowercase().as_str() {
            "day" => Ok(TimeGranularity::Day),
            "hour" => Ok(TimeGranularity::Hour),
            "minute" => Ok(TimeGranularity::Minute),
            other => Err(format!("Unsupported time_granularity '{}'", other)),
        }
    }
}

impl TimeGranularity {
    pub(crate) fn format_label(&self, datetime: DateTime<Utc>) -> String {
        match self {
            TimeGranularity::Day => datetime.format("%Y-%m-%d").to_string(),
            TimeGranularity::Hour => datetime.format("%Y-%m-%d %H:00").to_string(),
            TimeGranularity::Minute => datetime.format("%Y-%m-%d %H:%M").to_string(),
        }
    }
}

/// Supported aggregation strategies after grouping rows.
#[derive(Debug, PartialEq, Eq)]
#[doc(hidden)]
pub enum AggregationStrategy {
    /// Running sum that carries previous bucket totals forward.
    CumulativeSum,
}

impl FromStr for AggregationStrategy {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.to_lowercase().as_str() {
            "cumulative_sum" => Ok(AggregationStrategy::CumulativeSum),
            other => Err(format!("Unsupported aggregation_strategy '{}'", other)),
        }
    }
}

/// Configuration parameters for grouping and aggregating the fetched rows.
#[derive(Debug)]
#[doc(hidden)]
pub struct PostProcessingConfig {
    /// Optional column used for grouping the result set.
    pub group_by: Option<String>,
    /// Optional time granularity that formats timestamp-based buckets.
    pub time_granularity: Option<TimeGranularity>,
    /// Optional column used to compute aggregation metrics.
    pub aggregation_column: Option<String>,
    /// Optional strategy that determines how aggregation values are accumulated.
    pub aggregation_strategy: Option<AggregationStrategy>,
    /// When true, duplicate aggregation values are deduplicated before summing.
    pub dedup_aggregation: bool,
}

/// Sort options for gateway fetch: column and direction.
#[derive(Clone, Debug)]
pub struct SortOptions {
    /// Column name (normalized when gateway_force_camel_case_to_snake_case is enabled).
    pub column: String,
    /// When true, sort ascending; when false, sort descending.
    pub ascending: bool,
}

impl PostProcessingConfig {
    /// Builds post-processing preferences from the request payload.
    #[doc(hidden)]
    pub fn from_body(body: Option<&Value>, force_snake: bool) -> Self {
        let normalize = |value: &str| normalize_column_name(value, force_snake);
        let group_by: Option<String> = body
            .and_then(|b| b.get("group_by"))
            .and_then(Value::as_str)
            .map(normalize);
        let aggregation_column: Option<String> = body
            .and_then(|b| b.get("aggregation_column"))
            .and_then(Value::as_str)
            .map(normalize);
        let time_granularity: Option<TimeGranularity> = body
            .and_then(|b| b.get("time_granularity"))
            .and_then(Value::as_str)
            .and_then(|s| s.parse::<TimeGranularity>().ok());
        let aggregation_strategy: Option<AggregationStrategy> = body
            .and_then(|b| b.get("aggregation_strategy"))
            .and_then(Value::as_str)
            .and_then(|s| s.parse::<AggregationStrategy>().ok());
        let dedup_aggregation: bool = body
            .and_then(|b| b.get("aggregation_dedup"))
            .and_then(Value::as_bool)
            .unwrap_or(false);

        PostProcessingConfig {
            group_by,
            time_granularity,
            aggregation_column,
            aggregation_strategy,
            dedup_aggregation,
        }
    }
}
