use crate::cube_bridge::base_query_options::FilterValue;
use crate::planner::sql_templates::PlanSqlTemplates;
use cubenativeutils::CubeError;
use lazy_static::lazy_static;
use regex::{Captures, Regex};
use std::collections::HashMap;

//const PARAMS_MATCH_REGEXP = /\$(\d+)\$/g;
lazy_static! {
    static ref PARAMS_MATCH_RE: Regex = Regex::new(r"\$_(\d+)_\$").unwrap();
}
pub struct ParamsAllocator {
    params: Vec<FilterValue>,
    export_annotated_sql: bool,
}

impl ParamsAllocator {
    pub fn new(export_annotated_sql: bool) -> ParamsAllocator {
        ParamsAllocator {
            params: Vec::new(),
            export_annotated_sql,
        }
    }

    pub fn make_placeholder(&self, index: usize) -> String {
        format!("$_{}_$", index)
    }

    pub fn allocate_param(&mut self, name: &str) -> String {
        // Params allocated during Rust-side planning are already normalized to a
        // string at the call site, so they enter the channel as `FilterValue::Str`.
        // Native params (from the JS side) join later in `build_sql_and_params`
        // and keep their natural type, including `Null`.
        self.params.push(FilterValue::Str(name.to_string()));
        self.make_placeholder(self.params.len() - 1)
    }

    pub fn get_params(&self) -> &Vec<FilterValue> {
        &self.params
    }

    pub fn build_sql_and_params(
        &self,
        sql: &str,
        native_allocated_params: Vec<FilterValue>,
        should_reuse_params: bool,
        templates: &PlanSqlTemplates,
    ) -> Result<(String, Vec<FilterValue>), CubeError> {
        let (sql, params) = self.add_native_allocated_params(sql, &native_allocated_params)?;

        let mut params_in_sql_order = Vec::new();
        let mut param_index_map: HashMap<usize, usize> = HashMap::new();
        let mut error = None;

        let placeholder = |index: usize, error: &mut Option<CubeError>| {
            if self.export_annotated_sql {
                return format!("${}$", index);
            }
            match templates.param(index) {
                Ok(res) => res,
                Err(e) => {
                    if error.is_none() {
                        *error = Some(e);
                    }
                    "$error$".to_string()
                }
            }
        };

        let result_sql = if should_reuse_params {
            PARAMS_MATCH_RE
                .replace_all(&sql, |caps: &Captures| {
                    let ind: usize = caps[1].to_string().parse().unwrap();
                    let new_index = if let Some(index) = param_index_map.get(&ind) {
                        *index
                    } else {
                        let index = params_in_sql_order.len();
                        params_in_sql_order.push(params[ind].clone());
                        param_index_map.insert(ind, index);
                        index
                    };
                    placeholder(new_index, &mut error)
                })
                .to_string()
        } else {
            PARAMS_MATCH_RE
                .replace_all(&sql, |caps: &Captures| {
                    let ind: usize = caps[1].to_string().parse().unwrap();
                    let index = params_in_sql_order.len();
                    params_in_sql_order.push(params[ind].clone());
                    placeholder(index, &mut error)
                })
                .to_string()
        };
        if let Some(error) = error {
            return Err(error);
        }
        Ok((result_sql, params_in_sql_order))
    }

    fn add_native_allocated_params(
        &self,
        sql: &str,
        native_allocated_params: &[FilterValue],
    ) -> Result<(String, Vec<FilterValue>), CubeError> {
        lazy_static! {
            static ref NATIVE_PARAMS_MATCH_RE: Regex = Regex::new(r"\$(\d+)\$").unwrap();
        }

        if native_allocated_params.is_empty() {
            Ok((sql.to_string(), self.params.clone()))
        } else {
            let mut result_params = self.params.clone();
            // Occurrences of the same native param index must map to the same
            // internal param: databases like Postgres require expressions in
            // GROUP BY and window PARTITION BY/ORDER BY to be strictly equal,
            // and that includes param placeholders
            let mut native_index_map: HashMap<usize, usize> = HashMap::new();
            let sql = NATIVE_PARAMS_MATCH_RE
                .replace_all(sql, |caps: &Captures| {
                    let ind: usize = caps[1].to_string().parse().unwrap();
                    let internal_ind = *native_index_map.entry(ind).or_insert_with(|| {
                        result_params.push(native_allocated_params[ind].clone());
                        result_params.len() - 1
                    });
                    self.make_placeholder(internal_ind)
                })
                .to_string();
            Ok((sql, result_params))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_fixtures::cube_bridge::{MockDriverTools, MockSqlTemplatesRender};
    use std::rc::Rc;

    /// Dialect rendering params as positional `?`, i.e. everything but Postgres.
    fn positional_templates() -> PlanSqlTemplates {
        let driver_tools = MockDriverTools::with_sql_templates(
            MockSqlTemplatesRender::default_templates_with_positional_params(),
        )
        .without_params_reuse();
        PlanSqlTemplates::try_new(Rc::new(driver_tools), false).unwrap()
    }

    fn allocator_with_two_params(export_annotated_sql: bool) -> ParamsAllocator {
        let mut allocator = ParamsAllocator::new(export_annotated_sql);
        allocator.allocate_param("alpha");
        allocator.allocate_param("beta");
        allocator
    }

    #[test]
    fn positional_params_render_one_placeholder_per_param() {
        let allocator = allocator_with_two_params(false);

        let (sql, params) = allocator
            .build_sql_and_params(
                "SELECT $_0_$, $_0_$, $_1_$",
                vec![],
                false,
                &positional_templates(),
            )
            .unwrap();

        assert_eq!(sql, "SELECT ?, ?, ?");
        assert_eq!(
            params,
            vec![
                FilterValue::Str("alpha".to_string()),
                FilterValue::Str("alpha".to_string()),
                FilterValue::Str("beta".to_string()),
            ]
        );
    }

    #[test]
    fn annotated_sql_keeps_placeholders_unrendered_without_reuse() {
        let allocator = allocator_with_two_params(true);

        // The SQL API asks for annotated SQL so that cubesql can re-allocate the
        // params itself; the dialect placeholder must not be rendered here.
        let (sql, params) = allocator
            .build_sql_and_params(
                "SELECT $_0_$, $_0_$, $_1_$",
                vec![],
                false,
                &positional_templates(),
            )
            .unwrap();

        assert_eq!(sql, "SELECT $0$, $1$, $2$");
        assert_eq!(params.len(), 3);
    }

    #[test]
    fn annotated_sql_keeps_placeholders_unrendered_with_reuse() {
        let allocator = allocator_with_two_params(true);

        let (sql, params) = allocator
            .build_sql_and_params(
                "SELECT $_0_$, $_0_$, $_1_$",
                vec![],
                true,
                &positional_templates(),
            )
            .unwrap();

        assert_eq!(sql, "SELECT $0$, $0$, $1$");
        assert_eq!(params.len(), 2);
    }

    #[test]
    fn allocated_params_enter_channel_as_str() {
        let mut allocator = ParamsAllocator::new(false);
        let p0 = allocator.allocate_param("alpha");
        let p1 = allocator.allocate_param("beta");

        assert_eq!(p0, "$_0_$");
        assert_eq!(p1, "$_1_$");
        assert_eq!(
            allocator.get_params(),
            &vec![
                FilterValue::Str("alpha".to_string()),
                FilterValue::Str("beta".to_string()),
            ]
        );
    }

    #[test]
    fn native_params_keep_natural_type_including_null() {
        let mut allocator = ParamsAllocator::new(false);
        allocator.allocate_param("internal_a"); // $_0_$
        allocator.allocate_param("internal_b"); // $_1_$

        // Native placeholders use the single-`$` form; a `null` securityContext
        // field, a number, and a boolean must survive the merge with their type
        // intact — `Null` in particular must NOT collapse to an empty string.
        let sql = "SELECT $_0_$, $_1_$, $0$, $1$, $2$";
        let native = vec![
            FilterValue::Null,
            FilterValue::Num(42.0),
            FilterValue::Bool(true),
        ];

        let (rewritten_sql, params) = allocator
            .add_native_allocated_params(sql, &native)
            .expect("merge should succeed");

        // Native placeholders are rewritten into the internal `$_N_$` space,
        // appended after the two internal params.
        assert_eq!(rewritten_sql, "SELECT $_0_$, $_1_$, $_2_$, $_3_$, $_4_$");
        assert_eq!(
            params,
            vec![
                FilterValue::Str("internal_a".to_string()),
                FilterValue::Str("internal_b".to_string()),
                FilterValue::Null,
                FilterValue::Num(42.0),
                FilterValue::Bool(true),
            ]
        );
    }

    #[test]
    fn repeated_native_param_maps_to_single_internal_param() {
        let allocator = ParamsAllocator::new(false);

        // The same native param referenced from several member expressions
        // (e.g. a dimension and window function PARTITION BY inside a measure)
        // must stay a single param, otherwise Postgres rejects the query with
        // "must appear in the GROUP BY clause" as the expressions stop being equal.
        let sql = "SELECT TO_CHAR(d, $0$) x, LAG(m) OVER (PARTITION BY TO_CHAR(d, $0$)), $1$";
        let native = vec![
            FilterValue::Str("month".to_string()),
            FilterValue::Str("other".to_string()),
        ];

        let (rewritten_sql, params) = allocator
            .add_native_allocated_params(sql, &native)
            .expect("merge should succeed");

        assert_eq!(
            rewritten_sql,
            "SELECT TO_CHAR(d, $_0_$) x, LAG(m) OVER (PARTITION BY TO_CHAR(d, $_0_$)), $_1_$"
        );
        assert_eq!(
            params,
            vec![
                FilterValue::Str("month".to_string()),
                FilterValue::Str("other".to_string()),
            ]
        );
    }
}
