use crate::cube_bridge::member_sql::MemberSql;
use crate::test_fixtures::cube_bridge::yaml::pre_aggregation_time_dimension::YamlPreAggregationTimeDimension;
use crate::test_fixtures::cube_bridge::{
    MockMemberSql, MockPreAggregationDescription, MockPreAggregationIndex,
    MockPreAggregationTimeDimension,
};
use cubenativeutils::CubeError;
use serde::Deserialize;
use std::rc::Rc;

#[derive(Debug, Deserialize)]
pub struct YamlPreAggregationDefinition {
    #[serde(rename = "type", default = "default_type")]
    pre_aggregation_type: String,
    #[serde(default)]
    granularity: Option<String>,
    #[serde(default)]
    sql_alias: Option<String>,
    #[serde(default)]
    external: Option<bool>,
    #[serde(default)]
    allow_non_strict_date_range_match: Option<bool>,
    #[serde(default)]
    measures: Option<Vec<String>>,
    #[serde(default)]
    dimensions: Option<Vec<String>>,
    #[serde(default)]
    time_dimension: Option<String>,
    #[serde(default)]
    time_dimensions: Option<Vec<YamlPreAggregationTimeDimension>>,
    #[serde(default)]
    segments: Option<Vec<String>>,
    #[serde(default)]
    #[allow(dead_code)]
    partition_granularity: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    refresh_key: Option<YamlRefreshKey>,
    #[serde(default)]
    #[allow(dead_code)]
    scheduled_refresh: Option<bool>,
    #[serde(default)]
    #[allow(dead_code)]
    incremental: Option<bool>,
    #[serde(default)]
    #[allow(dead_code)]
    build_range_start: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    build_range_end: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    use_original_sql_pre_aggregations: Option<bool>,
    #[serde(default)]
    #[allow(dead_code)]
    union_with_source_data: Option<bool>,
    #[serde(default)]
    indexes: Option<Vec<YamlIndex>>,
    #[serde(default)]
    rollups: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct YamlRefreshKey {
    #[serde(default)]
    sql: Option<String>,
    #[serde(default)]
    every: Option<String>,
    #[serde(default)]
    update_window: Option<String>,
}

#[derive(Debug, Deserialize)]
struct YamlIndex {
    name: String,
    columns: Vec<String>,
    #[serde(rename = "type", default = "default_index_type")]
    index_type: String,
}

fn default_index_type() -> String {
    "regular".to_string()
}

fn default_type() -> String {
    "rollup".to_string()
}

impl YamlPreAggregationDefinition {
    pub fn build(self, name: String) -> Rc<MockPreAggregationDescription> {
        let measure_references = self
            .measures
            .map(|m| build_array_references(m))
            .transpose()
            .expect("Failed to build measure references");

        let dimension_references = self
            .dimensions
            .map(|d| build_array_references(d))
            .transpose()
            .expect("Failed to build dimension references");

        let time_dimension_reference = self
            .time_dimension
            .map(|td| build_single_reference(td))
            .transpose()
            .expect("Failed to build time dimension reference");

        let time_dimension_references: Option<Vec<Rc<MockPreAggregationTimeDimension>>> = self
            .time_dimensions
            .map(|tds| tds.into_iter().map(|td| td.build()).collect());

        let segment_references = self
            .segments
            .map(|s| build_array_references(s))
            .transpose()
            .expect("Failed to build segment references");

        let rollup_references = self
            .rollups
            .map(|r| build_array_references(r))
            .transpose()
            .expect("Failed to build rollup references");

        let indexes = self
            .indexes
            .unwrap_or_default()
            .into_iter()
            .map(|i| MockPreAggregationIndex {
                name: i.name,
                columns: i.columns,
                index_type: i.index_type,
            })
            .collect::<Vec<_>>();

        // Mirror the JS default (CUBEJS_EXTERNAL_DEFAULT=true): rollup and
        // rollupJoin are external unless explicitly set otherwise.
        let external = self.external.or_else(|| {
            matches!(self.pre_aggregation_type.as_str(), "rollup" | "rollupJoin").then_some(true)
        });

        Rc::new(
            MockPreAggregationDescription::builder()
                .name(name)
                .pre_aggregation_type(self.pre_aggregation_type)
                .granularity(self.granularity)
                .sql_alias(self.sql_alias)
                .external(external)
                .allow_non_strict_date_range_match(self.allow_non_strict_date_range_match)
                .measure_references_opt(measure_references)
                .dimension_references_opt(dimension_references)
                .time_dimension_reference_opt(time_dimension_reference)
                .segment_references_opt(segment_references)
                .rollup_references_opt(rollup_references)
                .time_dimension_references(time_dimension_references)
                .indexes(indexes)
                .build(),
        )
    }
}

fn build_array_references(members: Vec<String>) -> Result<Rc<dyn MemberSql>, CubeError> {
    if members.iter().any(|m| m.contains('{')) {
        MockMemberSql::pre_agg_array_templates(members).map(|m| m as Rc<dyn MemberSql>)
    } else {
        MockMemberSql::pre_agg_array_refs(members).map(|m| m as Rc<dyn MemberSql>)
    }
}

fn build_single_reference(member: String) -> Result<Rc<dyn MemberSql>, CubeError> {
    // Template syntax like "{CUBE}.created_at" models JS references built via
    // string interpolation (e.g. `${CUBE}.created_at`), where the member name
    // is literal text rather than a symbol path.
    if member.contains('{') {
        MockMemberSql::new(member).map(|m| Rc::new(m) as Rc<dyn MemberSql>)
    } else {
        MockMemberSql::pre_agg_single_ref(member).map(|m| Rc::new(m) as Rc<dyn MemberSql>)
    }
}
