use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum SqlExpectation {
  Single(String),
  Multi(Vec<String>),
}

#[derive(Debug, Deserialize)]
pub struct ExpectBlock {
  pub success: bool,
  pub result: Option<serde_json::Value>,
  pub errors: Option<Vec<serde_json::Value>>,
  #[serde(default)]
  pub sql: Option<Vec<SqlExpectation>>,
}

impl ExpectBlock {
  /// Advanced SQL execution assertion algorithm ported from `assert.go`.
  /// This compares two arrays of strings, one containing {{uuid:name}} or {{timestamp}} placeholders,
  /// and the other containing actual executed database queries. It ensures that placeholder UUIDs
  /// are consistently mapped to the same actual UUIDs across all lines, and strictly validates line-by-line sequences.
  pub fn assert_sql(&self, actual: &[String]) -> Result<(), String> {
    let patterns = match &self.sql {
      Some(s) => s,
      None => return Ok(()),
    };

    if patterns.len() != actual.len() {
      return Err(format!(
        "Length mismatch: expected {} SQL executions, got {}.\nActual Execution Log:\n{}",
        patterns.len(),
        actual.len(),
        actual.join("\n")
      ));
    }

    let ws_re = Regex::new(r"\s+").unwrap();

    let types = HashMap::from([
      (
        "uuid",
        r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
      ),
      (
        "timestamp",
        r"\d{4}-\d{2}-\d{2}(?:[ T])\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+\d{2}(?::\d{2})?)?",
      ),
      ("integer", r"-?\d+"),
      ("float", r"-?\d+\.\d+"),
      ("text", r"(?:''|[^'])*"),
      ("json", r"(?:''|[^'])*"),
    ]);

    let mut seen: HashMap<String, String> = HashMap::new();
    let system_uuid = "00000000-0000-0000-0000-000000000000";

    // Placeholder regex: {{type:name}} or {{type}}
    let ph_rx = Regex::new(r"\{\{([a-z]+)(?:[:]([^}]+))?\}\}").unwrap();

    let clean_str = |s: &str| -> String {
      let mut s = ws_re.replace_all(s, " ").into_owned();
      for token in ["(", ")", ",", "{", "}", "\"", "=", "'"] {
        s = s.replace(&format!(" {}", token), token);
        s = s.replace(&format!("{} ", token), token);
      }
      s.trim().to_string()
    };

    for (i, pattern_expect) in patterns.iter().enumerate() {
      let aline_raw = &actual[i];
      let aline = clean_str(aline_raw);

      let pattern_str_raw = match pattern_expect {
        SqlExpectation::Single(s) => s.clone(),
        SqlExpectation::Multi(m) => m.join(" "),
      };

      let pattern_str = clean_str(&pattern_str_raw);

      let mut pp = regex::escape(&pattern_str);
      pp = pp.replace(r"\{\{", "{{").replace(r"\}\}", "}}");

      let mut cap_names = HashMap::new(); // cg_X -> var_name
      let mut group_idx = 0;

      let mut final_rx_str = String::new();
      let mut last_match = 0;

      let pp_clone = pp.clone();
      for caps in ph_rx.captures_iter(&pp_clone) {
        let full_match = caps.get(0).unwrap();
        final_rx_str.push_str(&pp[last_match..full_match.start()]);

        let type_name = caps.get(1).unwrap().as_str();
        let var_name = caps.get(2).map(|m| m.as_str());

        if let Some(name) = var_name {
          if let Some(val) = seen.get(name) {
            final_rx_str.push_str(&regex::escape(val));
          } else {
            let type_pattern = types.get(type_name).unwrap_or(&".*?");
            let cg_name = format!("cg_{}", group_idx);
            final_rx_str.push_str(&format!("(?P<{}>{})", cg_name, type_pattern));
            cap_names.insert(cg_name, name.to_string());
            group_idx += 1;
          }
        } else {
          let type_pattern = types.get(type_name).unwrap_or(&".*?");
          final_rx_str.push_str(&format!("(?:{})", type_pattern));
        }

        last_match = full_match.end();
      }
      final_rx_str.push_str(&pp[last_match..]);

      let final_rx = match Regex::new(&format!("^{}$", final_rx_str)) {
        Ok(r) => r,
        Err(e) => return Err(format!("Bad constructed regex: {} -> {}", final_rx_str, e)),
      };

      if let Some(captures) = final_rx.captures(&aline) {
        for (cg_name, var_name) in cap_names {
          if let Some(m) = captures.name(&cg_name) {
            let matched_str = m.as_str();
            if matched_str != system_uuid {
              seen.insert(var_name, matched_str.to_string());
            }
          }
        }
      } else {
        return Err(format!(
          "Line mismatched at execution sequence {}.\nExpected Pattern: {}\nActual SQL: {}\nRegex used: {}\nVariables Mapped: {:?}",
          i + 1,
          pattern_str,
          aline,
          final_rx_str,
          seen
        ));
      }
    }

    Ok(())
  }
}
