use super::common::CompiledMemberPath;
use super::deps::{self, symbol_deps};
use super::MemberSymbol;
use crate::planner::query_tools::QueryTools;
use crate::planner::state::State;
use crate::planner::time_dimension::Granularity;
use crate::planner::{GranularityHelper, QueryDateTime, QueryDateTimeHelper};
use chrono::Duration;
use chrono_tz::Tz;
use cubenativeutils::CubeError;
use std::rc::Rc;

/// `MemberSymbol::TimeDimension` body: a base time dimension viewed
/// at a chosen granularity and (optionally) restricted to a date
/// range. Not declared in the data model — created at query time on
/// top of an existing time dimension.
#[derive(Clone)]
pub struct TimeDimensionSymbol {
    pub(super) base_symbol: Rc<MemberSymbol>,
    pub(super) compiled_path: CompiledMemberPath,
    pub(super) granularity: Option<String>,
    pub(super) granularity_obj: Option<Granularity>,
    pub(super) date_range: Option<(String, String)>,
    pub(super) alias_suffix: String,
    pub(super) alias_override: Option<String>,
    /// The value arrives already timezone-converted from the source
    /// (a pre-aggregation rollup or an input CTE), so rendering must
    /// not apply the conversion again. Composes with other forms: a
    /// symbol may carry this together with future derived forms.
    pub(super) tz_converted_at_source: bool,
}

symbol_deps! {
    TimeDimensionSymbol {
        granularity_obj: dep,
        base_symbol: dep_transparent,
        compiled_path: skip,
        granularity: skip,
        date_range: skip,
        alias_suffix: skip,
        alias_override: skip,
        tz_converted_at_source: skip,
    }
}

impl TimeDimensionSymbol {
    pub fn new(
        base_symbol: Rc<MemberSymbol>,
        granularity: Option<String>,
        granularity_obj: Option<Granularity>,
        date_range: Option<(String, String)>,
    ) -> Rc<Self> {
        Self::new_with_alias(base_symbol, granularity, granularity_obj, date_range, None)
    }

    /// Like [`Self::new`] but with an explicit alias override (e.g. the SQL
    /// API's `memberToAlias` entry for the granularized member). When `None`,
    /// the alias falls back to `{base alias}_{granularity}`.
    pub fn new_with_alias(
        base_symbol: Rc<MemberSymbol>,
        granularity: Option<String>,
        granularity_obj: Option<Granularity>,
        date_range: Option<(String, String)>,
        alias_override: Option<String>,
    ) -> Rc<Self> {
        let name_suffix = if let Some(granularity) = &granularity {
            granularity.clone()
        } else {
            "day".to_string()
        };

        assert!(!alias_override
            .as_ref()
            .map(|a| a.is_empty())
            .unwrap_or(false));
        let alias = alias_override
            .clone()
            .unwrap_or_else(|| format!("{}_{}", base_symbol.alias(), name_suffix));
        let full_name = format!("{}_{}", base_symbol.full_name(), name_suffix);

        let compiled_path = CompiledMemberPath::new(
            base_symbol.compiled_path().cube().clone(),
            full_name,
            base_symbol.name().clone(),
            alias,
            base_symbol.path().clone(),
        );
        Rc::new(Self {
            base_symbol,
            compiled_path,
            granularity,
            granularity_obj,
            date_range,
            alias_suffix: name_suffix,
            alias_override,
            tz_converted_at_source: false,
        })
    }

    pub fn base_symbol(&self) -> &Rc<MemberSymbol> {
        &self.base_symbol
    }

    pub fn tz_converted_at_source(&self) -> bool {
        self.tz_converted_at_source
    }

    pub fn granularity(&self) -> &Option<String> {
        &self.granularity
    }

    pub fn has_granularity(&self) -> bool {
        self.granularity.is_some()
    }

    pub fn granularity_obj(&self) -> &Option<Granularity> {
        &self.granularity_obj
    }

    pub fn resolved_granularity(&self) -> Result<Option<String>, CubeError> {
        let res = if let Some(granularity_obj) = &self.granularity_obj {
            Some(granularity_obj.resolved_granularity()?)
        } else {
            None
        };
        Ok(res)
    }

    /// Returns a copy of the symbol with the granularity replaced;
    /// the base symbol and date range are preserved.
    pub fn change_granularity(
        &self,
        // FIXME: late compilation. `State` is threaded here only to reach the
        // `Compiler` and (re)compile a granularity during planning. Once
        // compilation happens in an early phase, this should take the
        // already-resolved granularity and drop the `State`/`Compiler` dependency.
        state: Rc<State>,
        new_granularity: Option<String>,
    ) -> Result<Rc<Self>, CubeError> {
        let evaluator_compiler_cell = state.compiler().clone();
        let mut evaluator_compiler = evaluator_compiler_cell.borrow_mut();

        let new_granularity_obj = GranularityHelper::make_granularity_obj(
            state.cube_evaluator().clone(),
            &mut evaluator_compiler,
            &&self.base_symbol.cube_name(),
            &self.base_symbol.name(),
            new_granularity.clone(),
        )?;
        let date_range_tuple = self.date_range.clone();
        Ok(self.derive(
            self.base_symbol.clone(),
            new_granularity.clone(),
            new_granularity_obj.clone(),
            date_range_tuple,
            None,
        ))
    }

    /// Another form of the same time dimension — a different
    /// granularity of it, or the member it references. Render marks
    /// describe where the value comes from, which such a re-wrap does
    /// not change, so they are carried over.
    fn derive(
        &self,
        base_symbol: Rc<MemberSymbol>,
        granularity: Option<String>,
        granularity_obj: Option<Granularity>,
        date_range: Option<(String, String)>,
        alias_override: Option<String>,
    ) -> Rc<Self> {
        let mut new = (*Self::new_with_alias(
            base_symbol,
            granularity,
            granularity_obj,
            date_range,
            alias_override,
        ))
        .clone();
        new.tz_converted_at_source = self.tz_converted_at_source;
        Rc::new(new)
    }

    pub fn compiled_path(&self) -> &CompiledMemberPath {
        &self.compiled_path
    }

    /// Full unique identifier of the symbol: cube path, base
    /// dimension name and the granularity suffix.
    pub fn full_name(&self) -> String {
        self.compiled_path.full_name().clone()
    }

    /// Granularity name appended to the base symbol's alias and full
    /// name (e.g. `day`, `month`). Defaults to `day` when no
    /// granularity is set.
    pub fn alias_suffix(&self) -> String {
        self.alias_suffix.clone()
    }

    pub fn alias(&self) -> String {
        self.compiled_path.alias().clone()
    }

    pub fn owned_by_cube(&self) -> bool {
        self.base_symbol.owned_by_cube()
    }

    pub fn get_dependencies(&self) -> Vec<Rc<MemberSymbol>> {
        deps::collect_deps(self)
    }

    pub fn date_range_vec(&self) -> Option<Vec<String>> {
        self.date_range.clone().map(|(from, to)| vec![from, to])
    }

    /// Dependencies of this symbol, with any time-dimension dep
    /// wrapped in a `TimeDimensionSymbol` carrying this symbol's
    /// granularity and date range. Non-time-dimension deps pass
    /// through unchanged.
    pub fn get_dependencies_as_time_dimensions(&self) -> Vec<Rc<MemberSymbol>> {
        self.get_dependencies()
            .into_iter()
            .map(|s| match s.as_ref() {
                MemberSymbol::Dimension(dimension_symbol) => {
                    if dimension_symbol.is_time() {
                        let result = Self::new_with_alias(
                            s.clone(),
                            self.granularity.clone(),
                            self.granularity_obj.clone(),
                            self.date_range.clone(),
                            self.alias_override.clone(),
                        );
                        MemberSymbol::new_time_dimension(result)
                    } else {
                        s.clone()
                    }
                }
                _ => s.clone(),
            })
            .collect()
    }

    pub fn cube_name(&self) -> String {
        self.compiled_path.cube_name().clone()
    }

    pub fn path(&self) -> &Vec<String> {
        self.compiled_path.path()
    }

    pub fn join_map(&self) -> &Option<Vec<Vec<String>>> {
        self.compiled_path.join_map()
    }

    pub fn is_multi_stage(&self) -> bool {
        self.base_symbol.is_multi_stage()
    }

    /// False if the granularity defines its own calendar SQL — the
    /// symbol then materially computes a value rather than forwarding
    /// to its base. Otherwise delegates to the base symbol.
    pub fn is_reference(&self) -> bool {
        if let Some(granularity_obj) = &self.granularity_obj {
            if granularity_obj.calendar_sql().is_some() {
                return false;
            }
        }

        self.base_symbol.is_reference()
    }

    /// The base member this time dimension references, re-wrapped in
    /// a `TimeDimensionSymbol` with the same granularity and date
    /// range. `None` if the base is not a reference.
    pub fn reference_member(&self) -> Option<Rc<MemberSymbol>> {
        if let Some(base_symbol) = self.base_symbol.clone().reference_member() {
            let new_time_dim = self.derive(
                base_symbol,
                self.granularity.clone(),
                self.granularity_obj.clone(),
                self.date_range.clone(),
                self.alias_override.clone(),
            );
            Some(MemberSymbol::new_time_dimension(new_time_dim))
        } else {
            None
        }
    }

    pub fn name(&self) -> String {
        self.compiled_path.name().clone()
    }

    /// Minimum granularity implied by the `date_range` bounds — the
    /// smallest unit that still fully contains both endpoints.
    pub fn date_range_granularity(
        &self,
        query_tools: Rc<QueryTools>,
    ) -> Result<Option<String>, CubeError> {
        if let Some(date_range) = &self.date_range {
            let tz = query_tools.timezone();
            let from_date_str = QueryDateTimeHelper::format_from_date(&date_range.0, 3)?;
            let to_date_str = QueryDateTimeHelper::format_to_date(&date_range.1, 3)?;
            let start = QueryDateTime::from_date_str(tz, &from_date_str)?;
            let end = QueryDateTime::from_date_str(tz, &to_date_str)?;
            let end = end.add_duration(Duration::milliseconds(1))?;
            let start_granularity = start.granularity();
            let end_granularity = end.granularity();
            GranularityHelper::min_granularity(&Some(start_granularity), &Some(end_granularity))
        } else {
            Ok(None)
        }
    }

    /// Granularity to roll up to when matching this time dimension
    /// against a pre-aggregation. For predefined granularities or
    /// custom granularities not aligned with the requested date range,
    /// returns the minimum granularity needed to cover both.
    pub fn rollup_granularity(
        &self,
        query_tools: Rc<QueryTools>,
    ) -> Result<Option<String>, CubeError> {
        if let Some(granularity_obj) = &self.granularity_obj {
            if let Some(date_range) = &self.date_range {
                let date_range_granularity = self.date_range_granularity(query_tools.clone())?;

                // For predefined granularities or custom granularities not aligned with date range,
                // we need to return the minimum granularity
                if granularity_obj.is_predefined_granularity() {
                    let self_granularity = granularity_obj.min_granularity()?;
                    GranularityHelper::min_granularity(&date_range_granularity, &self_granularity)
                } else {
                    let from_date_str = QueryDateTimeHelper::format_from_date(&date_range.0, 3)?;
                    let to_date_str = QueryDateTimeHelper::format_to_date(&date_range.1, 3)?;
                    let is_aligned = granularity_obj.is_aligned_with_date_range(
                        &from_date_str,
                        &to_date_str,
                        query_tools.timezone(),
                    )?;

                    if is_aligned {
                        Ok(self.granularity.clone())
                    } else {
                        let self_granularity = granularity_obj.min_granularity()?;
                        GranularityHelper::min_granularity(
                            &date_range_granularity,
                            &self_granularity,
                        )
                    }
                }
            } else {
                Ok(self.granularity.clone())
            }
        } else {
            let date_range_granularity = self.date_range_granularity(query_tools.clone())?;
            Ok(date_range_granularity)
        }
    }

    /// Adjusts `date_range` so the start aligns with the
    /// granularity's origin when the granularity is custom
    /// (non-predefined). Predefined granularities and missing ranges
    /// pass through unchanged.
    pub fn get_range_for_time_series(
        &self,
        date_range: Option<Vec<String>>,
        tz: Tz,
    ) -> Result<Option<(String, String)>, CubeError> {
        let res = if let Some(date_range) = &date_range {
            if date_range.len() != 2 {
                return Err(CubeError::user(format!(
                    "Invalid date range: {:?}",
                    date_range
                )));
            } else {
                if let Some(granularity_obj) = &self.granularity_obj {
                    if !granularity_obj.is_predefined_granularity() {
                        let start = QueryDateTime::from_date_str(tz, &date_range[0])?;
                        let start = granularity_obj.align_date_to_origin(start)?;
                        let end = QueryDateTime::from_date_str(tz, &date_range[1])?;

                        Some((start.to_string(), end.to_string()))
                    } else {
                        Some((date_range[0].clone(), date_range[1].clone()))
                    }
                } else {
                    Some((date_range[0].clone(), date_range[1].clone()))
                }
            }
        } else {
            None
        };
        Ok(res)
    }
}

impl crate::utils::debug::DebugSql for TimeDimensionSymbol {
    fn debug_sql(&self, expand_deps: bool) -> String {
        let sql = if expand_deps {
            self.base_symbol.debug_sql(expand_deps)
        } else {
            format!("{{{}}}", self.base_symbol.full_name())
        };

        if let Some(granularity) = &self.granularity {
            format!("({}).{}", sql, granularity)
        } else {
            sql
        }
    }
}
