use std::sync::Arc;

use crate::builtins::meta::type_of;
use crate::calcit::{Calcit, CalcitErr, CalcitErrKind, CalcitList, CalcitProc, CalcitRecord, format_proc_examples_hint};

use crate::util::number::is_even;

pub fn call_new_map(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if is_even(xs.len()) {
    let n = xs.len() >> 1;
    let mut ys = rpds::HashTrieMap::new_sync();
    for i in 0..n {
      ys.insert_mut(xs[i << 1].to_owned(), xs[(i << 1) + 1].to_owned());
    }
    Ok(Calcit::Map(ys))
  } else {
    let msg = format!(
      "&{{}} requires an even number of arguments (key-value pairs), but received: {} arguments",
      xs.len()
    );
    let hint = format_proc_examples_hint(&CalcitProc::NativeMap).unwrap_or_default();
    CalcitErr::err_str_with_hint(CalcitErrKind::Arity, msg, hint)
  }
}

pub fn dissoc(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() < 2 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:dissoc expected at least 2 arguments, but received:", xs);
  }
  match xs.first() {
    Some(Calcit::Map(base)) => {
      let ys = &mut base.to_owned();
      let mut skip_first = true;
      for x in xs {
        if skip_first {
          skip_first = false;
          continue;
        }
        ys.remove_mut(x);
      }
      Ok(Calcit::Map(ys.to_owned()))
    }
    Some(a) => {
      let msg = format!("&map:dissoc requires a map, but received: {}", type_of(&[a.to_owned()])?.lisp_str());
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapDissoc).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    _ => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:dissoc expected 2 arguments, but received:", xs),
  }
}

pub fn get(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(a)) => {
      let ys = &mut xs.to_owned();
      match ys.get(a) {
        Some(v) => Ok(v.to_owned()),
        None => Ok(Calcit::Nil),
      }
    }
    (Some(_), None) => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapGet).unwrap_or_default();
      CalcitErr::err_nodes_with_hint(
        CalcitErrKind::Arity,
        "&map:get requires 2 arguments (map and key), but received:",
        xs,
        hint,
      )
    }
    (Some(a), Some(_)) => {
      let msg = format!("&map:get requires a map, but received: {}", type_of(&[a.to_owned()])?.lisp_str());
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapGet).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    (None, ..) => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapGet).unwrap_or_default();
      CalcitErr::err_nodes_with_hint(
        CalcitErrKind::Arity,
        "&map:get requires 2 arguments (map and key), but received:",
        xs,
        hint,
      )
    }
  }
}

pub fn call_merge(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() == 2 {
    match (&xs[0], &xs[1]) {
      (Calcit::Map(xs), Calcit::Nil) => Ok(Calcit::Map(xs.to_owned())),
      (Calcit::Map(xs), Calcit::Map(ys)) => {
        let mut zs: rpds::HashTrieMapSync<Calcit, Calcit> = xs.to_owned();
        for (k, v) in ys {
          zs.insert_mut(k.to_owned(), v.to_owned());
        }
        Ok(Calcit::Map(zs))
      }
      (Calcit::Record(record @ CalcitRecord { struct_ref, values }), Calcit::Map(ys)) => {
        let mut new_values = (**values).to_owned();
        for (k, v) in ys {
          match k {
            Calcit::Str(s) | Calcit::Symbol { sym: s, .. } => match record.index_of(s) {
              Some(pos) => v.clone_into(&mut new_values[pos]),
              None => {
                return CalcitErr::err_str(
                  CalcitErrKind::Type,
                  format!("&map:merge invalid field `{s}` for record: {:?}", struct_ref.fields),
                );
              }
            },
            Calcit::Tag(s) => match record.index_of(s.ref_str()) {
              Some(pos) => v.clone_into(&mut new_values[pos]),
              None => {
                return CalcitErr::err_str(
                  CalcitErrKind::Type,
                  format!("&map:merge invalid field `{s}` for record: {:?}", struct_ref.fields),
                );
              }
            },
            a => return CalcitErr::err_str(CalcitErrKind::Type, format!("&map:merge invalid field key, but received: {a}")),
          }
        }
        Ok(Calcit::Record(CalcitRecord {
          struct_ref: record.struct_ref.to_owned(),
          values: Arc::new(new_values),
        }))
      }
      (a, b) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:merge expected 2 maps, but received: {a} {b}")),
    }
  } else {
    CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:merge expected 2 arguments, but received:", xs)
  }
}

/// to set
pub fn to_pairs(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    // get a random order from internals
    Some(Calcit::Map(ys)) => {
      let mut zs: rpds::HashTrieSetSync<Calcit> = rpds::HashTrieSet::new_sync();
      for (k, v) in ys {
        let chunk = vec![k.to_owned(), v.to_owned()];
        zs.insert_mut(Calcit::from(chunk));
      }
      Ok(Calcit::Set(zs))
    }
    Some(Calcit::Record(CalcitRecord { struct_ref, values, .. })) => {
      let mut zs: rpds::HashTrieSetSync<Calcit> = rpds::HashTrieSet::new_sync();
      for idx in 0..struct_ref.fields.len() {
        let chunk = vec![Calcit::Tag(struct_ref.fields[idx].to_owned()), values[idx].to_owned()];
        zs.insert_mut(Calcit::from(chunk));
      }
      Ok(Calcit::Set(zs))
    }
    Some(a) => {
      let msg = format!(
        "&map:to-pairs requires a map or record, but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = format_proc_examples_hint(&CalcitProc::ToPairs).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    None => {
      let hint = format_proc_examples_hint(&CalcitProc::ToPairs).unwrap_or_default();
      CalcitErr::err_str_with_hint(
        CalcitErrKind::Arity,
        "&map:to-pairs requires 1 argument, but received none".to_string(),
        hint,
      )
    }
  }
}

pub fn map_keys(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(ys)) => {
      let keys: Vec<Calcit> = ys.keys().map(|k| k.to_owned()).collect();
      Ok(Calcit::List(Arc::new(CalcitList::Vector(keys))))
    }
    Some(a) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:keys expected a map, but received: {a}")),
    None => CalcitErr::err_str(CalcitErrKind::Arity, "&map:keys expected 1 argument, but received none"),
  }
}

pub fn map_vals(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(ys)) => {
      let vals: Vec<Calcit> = ys.values().map(|v| v.to_owned()).collect();
      Ok(Calcit::List(Arc::new(CalcitList::Vector(vals))))
    }
    Some(a) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:vals expected a map, but received: {a}")),
    None => CalcitErr::err_str(CalcitErrKind::Arity, "&map:vals expected 1 argument, but received none"),
  }
}

pub fn call_merge_non_nil(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(Calcit::Map(ys))) => {
      let mut zs: rpds::HashTrieMapSync<Calcit, Calcit> = xs.to_owned();
      for (k, v) in ys {
        if *v != Calcit::Nil {
          zs.insert_mut(k.to_owned(), v.to_owned());
        }
      }
      Ok(Calcit::Map(zs))
    }
    (Some(a), Some(b)) => {
      let msg = format!(
        "&map:merge-non-nil requires 2 maps, but received: {} and {}",
        type_of(&[a.to_owned()])?.lisp_str(),
        type_of(&[b.to_owned()])?.lisp_str()
      );
      let hint = format_proc_examples_hint(&CalcitProc::NativeMergeNonNil).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    (_, _) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:merge-non-nil expected 2 arguments, but received:", xs),
  }
}

/// out to list, but with a arbitrary order
pub fn to_list(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(m)) => {
      let mut ys = vec![];
      for (k, v) in m {
        let zs = vec![k.to_owned(), v.to_owned()];
        ys.push(Calcit::from(zs));
      }
      Ok(Calcit::from(ys))
    }
    Some(a) => {
      let msg = format!(
        "&map:to-list requires a map, but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapToList).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    None => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapToList).unwrap_or_default();
      CalcitErr::err_str_with_hint(
        CalcitErrKind::Arity,
        "&map:to-list requires 1 argument, but received none".to_string(),
        hint,
      )
    }
  }
}

pub fn count(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(ys)) => Ok(Calcit::Number(ys.size() as f64)),
    Some(a) => {
      let msg = format!("&map:count requires a map, but received: {}", type_of(&[a.to_owned()])?.lisp_str());
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapCount).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    None => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapCount).unwrap_or_default();
      CalcitErr::err_str_with_hint(
        CalcitErrKind::Arity,
        "&map:count requires 1 argument, but received none".to_string(),
        hint,
      )
    }
  }
}

pub fn empty_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(ys)) => Ok(Calcit::Bool(ys.is_empty())),
    Some(a) => {
      let msg = format!("&map:empty? requires a map, but received: {}", type_of(&[a.to_owned()])?.lisp_str());
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapEmpty).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    None => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapEmpty).unwrap_or_default();
      CalcitErr::err_str_with_hint(
        CalcitErrKind::Arity,
        "&map:empty? requires 1 argument, but received none".to_string(),
        hint,
      )
    }
  }
}

pub fn contains_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(a)) => Ok(Calcit::Bool(xs.contains_key(a))),
    (Some(_), None) => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapContains).unwrap_or_default();
      CalcitErr::err_nodes_with_hint(
        CalcitErrKind::Arity,
        "&map:contains? requires 2 arguments (map and key), but received:",
        xs,
        hint,
      )
    }
    (Some(a), Some(_)) => {
      let msg = format!(
        "&map:contains? requires a map, but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapContains).unwrap_or_default();
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
    (None, ..) => {
      let hint = format_proc_examples_hint(&CalcitProc::NativeMapContains).unwrap_or_default();
      CalcitErr::err_nodes_with_hint(
        CalcitErrKind::Arity,
        "&map:contains? requires 2 arguments (map and key), but received:",
        xs,
        hint,
      )
    }
  }
}

pub fn includes_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(ys)), Some(a)) => {
      for (_k, v) in ys {
        if v == a {
          return Ok(Calcit::Bool(true));
        }
      }
      Ok(Calcit::Bool(false))
    }
    (Some(a), ..) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:includes? expected a map, but received: {a}")),
    (None, ..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:includes? expected 2 arguments, but received:", xs),
  }
}

pub fn destruct(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(ys)) => match ys.keys().next() {
      // order not stable
      Some(k0) => {
        let mut zs = ys.to_owned();
        zs.remove_mut(k0);
        Ok(Calcit::from(CalcitList::from(&[k0.to_owned(), ys[k0].to_owned(), Calcit::Map(zs)])))
      }
      None => Ok(Calcit::Nil),
    },
    Some(a) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:destruct expected a map, but received: {a}")),
    None => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:destruct expected 1 argument, but received:", xs),
  }
}

pub fn assoc(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Map(base)) => {
      if xs.len() % 2 != 1 {
        CalcitErr::err_nodes(
          CalcitErrKind::Arity,
          "&map:assoc expected an odd number of arguments, but received:",
          xs,
        )
      } else {
        let size = (xs.len() - 1) / 2;
        let mut ys = base.to_owned();
        for idx in 0..size {
          ys.insert_mut(xs[idx * 2 + 1].to_owned(), xs[idx * 2 + 2].to_owned());
        }
        Ok(Calcit::Map(ys))
      }
    }
    Some(a) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:assoc expected a map, but received: {a}")),
    None => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:assoc expected 3 arguments, but received:", xs),
  }
}

pub fn diff_new(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(Calcit::Map(ys))) => {
      // Build result directly instead of cloning xs and removing keys
      let mut zs: rpds::HashTrieMapSync<Calcit, Calcit> = rpds::HashTrieMap::new_sync();
      for (k, v) in xs.iter() {
        if !ys.contains_key(k) {
          zs.insert_mut(k.to_owned(), v.to_owned());
        }
      }
      Ok(Calcit::Map(zs))
    }
    (Some(a), Some(b)) => CalcitErr::err_str(CalcitErrKind::Type, format!("&map:diff-new expected 2 maps, but received: {a} {b}")),
    (..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:diff-new expected 2 arguments, but received:", xs),
  }
}

pub fn diff_keys(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(Calcit::Map(ys))) => {
      let mut ks: rpds::HashTrieSetSync<Calcit> = rpds::HashTrieSet::new_sync();
      for k in xs.keys() {
        if !ys.contains_key(k) {
          ks.insert_mut(k.to_owned());
        }
      }
      Ok(Calcit::Set(ks))
    }
    (Some(a), Some(b)) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&map:diff-keys expected 2 maps, but received: {a} {b}"),
    ),
    (..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:diff-keys expected 2 arguments, but received:", xs),
  }
}

pub fn common_keys(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(xs)), Some(Calcit::Map(ys))) => {
      let mut ks: rpds::HashTrieSetSync<Calcit> = rpds::HashTrieSet::new_sync();
      for k in xs.keys() {
        if ys.contains_key(k) {
          ks.insert_mut(k.to_owned());
        }
      }
      Ok(Calcit::Set(ks))
    }
    (Some(a), Some(b)) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&map:common-keys expected 2 maps, but received: {a} {b}"),
    ),
    (..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:common-keys expected 2 arguments, but received:", xs),
  }
}

/// Single-pass diff: `(&map:diff-triple a b)` → `[drop-keys new-diff common-triples]`
///
/// - `drop-keys`: Set of keys in `a` but not in `b`
/// - `new-diff`: Map of entries in `b` but not in `a`
/// - `common-triples`: List of `[k va vb]` for every key present in both maps
///
/// Replaces three separate calls to `&map:diff-keys`, `&map:diff-new`, and `&map:common-keys`
/// followed by `&map:to-list` + per-key `&map:get`, reducing map traversals from 3+ to 2.
pub fn diff_triple(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Map(a)), Some(Calcit::Map(b))) => {
      let mut drop_keys: rpds::HashTrieSetSync<Calcit> = rpds::HashTrieSet::new_sync();
      let mut common_triples: Vec<Calcit> = Vec::new();

      // One pass over a: split into drop-keys and common-triples
      for (k, va) in a.iter() {
        match b.get(k) {
          Some(vb) => {
            let triple = Calcit::from(CalcitList::from(&[k.to_owned(), va.to_owned(), vb.to_owned()]));
            common_triples.push(triple);
          }
          None => {
            drop_keys.insert_mut(k.to_owned());
          }
        }
      }

      // One pass over b: collect entries not in a
      let mut new_diff: rpds::HashTrieMapSync<Calcit, Calcit> = rpds::HashTrieMap::new_sync();
      for (k, vb) in b.iter() {
        if !a.contains_key(k) {
          new_diff.insert_mut(k.to_owned(), vb.to_owned());
        }
      }

      let result = CalcitList::from(&[Calcit::Set(drop_keys), Calcit::Map(new_diff), Calcit::from(common_triples)]);
      Ok(Calcit::from(result))
    }
    (Some(a), Some(b)) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&map:diff-triple expected 2 maps, but received: {a} {b}"),
    ),
    (..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&map:diff-triple expected 2 arguments, but received:", xs),
  }
}
